第二节: react-redux库、redux-thunk库、devtools工具的使用 、reducer模块划分
一. react-redux库
1. 背景
在仅仅导入【redux】库的情况下,代码非常冗余,每个组件中都需要:
(1). 需要在componentDidMount生命周期中subscribe订阅, store中的数据修改时候,给页面state中的数据进行修改赋值
(2). 默认构造函数中需要给 state中进行从store中获取数据进行赋值
详见:home.jsx profile.jsx
2. 说明
虽然我们之前已经实现了connect、Provider这些帮助我们完成连接redux、react的辅助工具,但是实际上redux官方帮助我们提供了 react-redux 的库,可以直接在项目中使用,并且实现的逻辑会更加的严谨和高效
【npm install react-redux】
实现的功能:
(1). 将state里的数据映射到props里,通过 this.props.xxx 获取, 简化了componentDidMount中subscribe订阅 和 constructor中的赋值。
(2). 将自定义方法映射到props里,通过 this.props.xxx 调用方法。
3. 实操
(1). 安装 【npm install react-redux】
(2). index 中的配置 <Provider> 包裹, 并给store属性赋值: store={store}
import store from "./store";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<Provider store={store}>
<App />
</Provider>
);
(3). 在ZAbout组件中:
A. 编写 mapStateToProps方法 和 mapDispatchToProps,用于将state里“需要的”数据和自定义的方法映射到props里
B. 调用connect方法进行执行 export default connect(mapStateToProps, mapDispatchToProps)(ZAbout);
PS:剖析connect方法:
connect是一个高阶函数,接收两个函数作为参数,connect(xxx,xxx)调用后的返回值还是一个函数。
这个函数是一个高阶组件 connect(xxx,xxx)(newComponent),接收一个组件作为参数, 返回值还是一个组件
补充
高阶函数:接收函数作为参数,返回值也是一个函数
高阶组件:高阶组件本身是一个函数,接收一个组件作为参数,返回值也是一个组件
(4). 在render中通过 this.props里拿到counter进行赋值显示即可
(5). 自定义方法进行 加减counter,通过this.props.xxx 调用 mapDispatchToProps 里映射的方法来修改 store里的counter
核心代码分享:
查看代码
import React, { PureComponent } from "react";
import { subNumAction, addNumAction } from "../store/actionCreators";
import { connect } from "react-redux";
export class ZAbout extends PureComponent {
subNumber(num) {
this.props.subNumber(num);
}
addNumber(num) {
this.props.addNumber(num);
}
render() {
const { counter, banners, recommends } = this.props;
return (
<div>
<h3>ZAbout Counter:{counter}</h3>
<div>
<button onClick={() => this.subNumber(1)}>-1</button>
<button onClick={() => this.subNumber(5)}>-5</button>
<button onClick={() => this.subNumber(8)}>-8</button>
<button onClick={() => this.addNumber(1)}>+1</button>
<button onClick={() => this.addNumber(5)}>+5</button>
<button onClick={() => this.addNumber(8)}>+8</button>
</div>
<div className="banner">
<h2>轮播图数据:</h2>
<ul>
{banners?.map((item, index) => (
<li key={index}>{item.title}</li>
))}
</ul>
</div>
<div className="recommend">
<h2>推荐数据:</h2>
<ul>
{recommends?.map((item, index) => (
<li key={index}>{item.title}</li>
))}
</ul>
</div>
</div>
);
}
}
//1. 将state里的数据映射到props里的封装
const mapStateToProps = state => ({
counter: state.counter,
// 下面的banners 和 recommends 都是zCategory1中获取后赋值到store里的
banners: state.banners,
recommends: state.recommends,
});
//2. 将下面定义的的方法映射到props里的封装
const mapDispatchToProps = dispatch => ({
addNumber(num) {
dispatch(addNumAction(num));
},
subNumber(num) {
dispatch(subNumAction(num));
},
});
//3. 执行
/*
connect是一个高阶函数,接收两个函数作为参数,connect(xxx,xxx)调用后的返回值还是一个函数。
这个函数是一个高阶组件 connect(xxx,xxx)(newComponent),接收一个组件作为参数, 返回值还是一个组件
高阶函数:接收函数作为参数,返回值也是一个函数
高阶组件:高阶组件本身是一个函数,接收一个组件作为参数,返回值也是一个组件
*/
export default connect(mapStateToProps, mapDispatchToProps)(ZAbout);
二. redux-thunk库
1. 背景--组件中的异步操作
真实开发中,redux中保存的很多数据可能来自服务器,我们需要进行异步的请求,再将数据保存到redux中,网络请求可以在class组件的componentDidMount中发送,如下图01:
1.1 实操-zCateGory1组件中
(1). 编写 mapStateToProps方法 和 mapDispatchToProps,用于将state里“需要的”数据和自定义的方法映射到props里
(2). 在componentDidMount发送异步请求获取数据,修改store里的数据banners和recommends数据
(3). 修改后zAbout组件中就可以通过mapStateToProps映射拿到store里的banners和recommends了
1.2 缺陷
我们必须将网络请求的异步代码放到组件的生命周期中来完成;
事实上,网络请求到的数据也属于我们状态管理的一部分,更好的一种方式应该是将其也交给redux来管理
代码分享:
查看代码
import React, { PureComponent } from "react";
import {
changeBannersAction,
changeRecommendsAction,
} from "../store/actionCreators";
import { connect } from "react-redux";
import axios from "axios";
// import store from "../store";
export class ZCategory1 extends PureComponent {
async componentDidMount() {
let { data } = await axios.get("http://xxx:8000/home/multidata");
const banners = data.data.banner.list;
const recommends = data.data.recommend.list;
// 修改store里的数据
this.props.changeBanners(banners);
this.props.changeRecommends(recommends);
// console.log(store.getState());
}
render() {
const { counter } = this.props;
return (
<div>
<h3>ZCategory1 Counter:{counter}</h3>
</div>
);
}
}
//1. 将state里的数据映射到props里的封装
const mapStateToProps = state => ({
counter: state.counter,
});
//2. 将下面生命的方法映射到props里的封装
const mapDispatchToProps = dispatch => ({
changeBanners(data) {
dispatch(changeBannersAction(data));
},
changeRecommends(data) {
dispatch(changeRecommendsAction(data));
},
});
//3. 执行
export default connect(mapStateToProps, mapDispatchToProps)(ZCategory1);
2. redux-thunk说明
(1). redux中如何可以进行异步的操作呢?
使用中间件, redux也引入了中间件(Middleware)的概念:
这个中间件的目的是在dispatch的action和最终达到的reducer之间,扩展一些自己的代码;比如日志记录、调用异步接口、添加代码调试功能等等
官网推荐:使用【redux-thunk】来实现网络请求中间件
详见图02
(2). redux-thunk 如何做到让我们可以发送异步的请求呢?
默认情况下的dispatch(action),action需要是一个JavaScript的对象,eg dispatch({type:"xxx",num:10})
redux-thunk可以让dispatch接收一个函数,即action可以是一个函数;该函数会被调用,并且会传给这个函数一个dispatch函数和getState函数;
A. dispatch函数用于我们之后再次派发action;
B. getState函数考虑到我们之后的一些操作需要依赖原来的状态,用于让我们可以获取之前的一些状态, 即:可以用来获取store里的数据;
3. 实操
详见:ZCategory2
(1). 安装 【npm install redux-thunk】
(2). 配置中间件
在创建store时传入应用了middleware的enhance函数,通过applyMiddleware来结合多个Middleware, 返回一个enhancer; 将enhancer作为第二个参数传入到createStore中
详见 store/index.js
import thunk from 'redux-thunk';
import reducer from './reducer';
import { applyMiddleware, createStore, compose } from 'redux';
// 1. 最原始的默认写法
// const store = createStore(reducer);
// 2. redux-thunk的使用
// 用于实现派发一个函数 dispatch(function)
const store = createStore(reducer, applyMiddleware(thunk));
export default store;
(3). actionCreators中配置
编写 fetchHomeMultidataAction方法,返回的也是一个函数,函数有两个参数:dispatch 和 getState
(4) ZCategory2组件
编写 mapStateToProps用来映射counter
编写 mapDispatchToProps 用来映射调用fetchHomeMultidataAction的方法
在 componentDidMount 中通过 this.props.fecthHomeMultidata() 调用即可
(5). App中注释掉 ZCategory1组件,引入ZCategory1,测试获取数据,在zAbout组件中使用
核心代码分享:
查看代码
import React, { PureComponent } from "react";
import { fetchHomeMultidataAction } from "../store/actionCreators";
import { connect } from "react-redux";
export class ZCategory2 extends PureComponent {
async componentDidMount() {
this.props.fetchHomeMultidata();
}
render() {
const { counter } = this.props;
return (
<div>
<h3>ZCategory2 Counter:{counter}</h3>
</div>
);
}
}
//1. 将state里的数据映射到props里的封装
const mapStateToProps = state => ({
counter: state.counter,
});
//2. 将下面声明的方法映射到props里的封装
const mapDispatchToProps = dispatch => ({
fetchHomeMultidata() {
dispatch(fetchHomeMultidataAction());
},
});
//3. 执行
export default connect(mapStateToProps, mapDispatchToProps)(ZCategory2);
三. devtools工具的使用
1. 在对应的浏览器中安装相关的插件(比如Chrome/Edge浏览器扩展商店中搜索Redux DevTools即可);
2. 在redux中继承devtools的中间件;
详见store/index (特别注意下面的 ?. 可选链的使用,否则在没有安装插件的浏览器上无法使用哦)
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__?.({ trace: true }) || compose;
const store = createStore(reducer, composeEnhancers(applyMiddleware(thunk)));
import thunk from 'redux-thunk';
import reducer from './reducer';
import { applyMiddleware, createStore, compose } from 'redux';
// 1. 最原始的默认写法
// const store = createStore(reducer);
// 2. redux-thunk的使用
// 用于实现派发一个函数 dispatch(function)
// const store = createStore(reducer, applyMiddleware(thunk));
// 3. redux-devtools的使用 (生产环境建议注释掉,不要把数据暴露出来)
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__?.({ trace: true }) || compose;
const store = createStore(reducer, composeEnhancers(applyMiddleware(thunk)));
export default store;
3. 调试图详见 03
四. reducer模块
1. 背景
当前这个reducer既有处理counter的代码,又有处理home页面的数据;
后续counter相关的状态或home相关的状态会进一步变得更加复杂;
我们也会继续添加其他的相关状态,比如购物车、分类、歌单等等;
如果将所有的状态都放到一个reducer中进行管理,随着项目的日趋庞大,必然会造成代码臃肿、难以维护。
2. reducer拆分
先抽取一个对counter处理的reducer;
再抽取一个对home处理的reducer;
将它们合并起来;
3. 文件拆分
虽然已经放到不同的函数了,但是这些函数的处理依然是在同一个文件中,代码非常的混乱;
另外关于reducer中用到的constant、action等我们也依然是在同一个文件中;
所以在store文件夹下拆分出来两个文件,count 和 home文件夹, 这两个文件夹下的index文件改为 对外导出reducer 和 actionCreators
import reducer from "./reducer";
// 对外导出reducer (默认导出的形式)
export default reducer;
// 对外导出所有的actionCreators
export * from "./actionCreators";
4. 整合
redux给我们提供了一个combineReducers函数可以方便的让我们对多个reducer进行合并:
那么combineReducers是如何实现的呢?
(1). 事实上,它也是将我们传入的reducers合并到一个对象中,最终返回一个combination的函数(相当于我们之前的reducer函数了);
(2). 在执行combination函数的过程中,它会通过判断前后返回的数据是否相同来决定返回之前的state还是新的state;
(3). 新的state会触发订阅者发生对应的刷新,而旧的state可以有效的组织订阅者发生刷新;
详见:store/index
import thunk from 'redux-thunk';
import { applyMiddleware, createStore, compose, combineReducers } from 'redux';
// 导入子reducer
import counterReducer from './count';
import homeReducer from './home';
// 合并reducer
const combineReducer = combineReducers({
childCount: counterReducer,
childHome: homeReducer,
});
// 1. 最原始的默认写法
// const store = createStore(combineReducer);
// 2. redux-thunk的使用
// 用于实现派发一个函数 dispatch(function)
// const store = createStore(combineReducer, applyMiddleware(thunk));
// 3. redux-devtools的使用 (生产环境建议注释掉,不要把数据暴露出来)
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__?.({ trace: true }) || compose;
const store = createStore(combineReducer, composeEnhancers(applyMiddleware(thunk)));
export default store;
其它代码的改造: 调用子reducer中state里的数据的时候,需要加上子名称
store.getState().childCount.counter
state.childCount.counter
!
- 作 者 : Yaopengfei(姚鹏飞)
- 博客地址 : http://www.cnblogs.com/yaopengfei/
- 声 明1 : 如有错误,欢迎讨论,请勿谩骂^_^。
- 声 明2 : 原创博客请在转载时保留原文链接或在文章开头加上本人博客地址,否则保留追究法律责任的权利。