在react中使用redux
这里我们创建两个组件,展示同一个count,分别在两个组件中对count进行操作。
项目目录结构:
安装redux:
npm install redux
代码:
// ./store/index.js
import {createStore } from "redux";
import reducer from "./reducer";
const store = createStore(reducer); // 创建store
export default store;
// ./store/reducer.js
import { ADD_COUNT, SUB_COUNT } from "./constants";
const initialState = {
count: 100 // 初始状态
}
function reducer(state= initialState,action){
switch(action.type){
case ADD_COUNT:
return { ...state,count: state.count + action.num };
case SUN_COUNT:
return { ...state,count: state.count - action.num };
default:
return state;
}
}
export default reducer;
// ./store/actionCreators.js
import * as actionTypes from "./constants";
export const addCountAction = (num) => ({
type: actionTypes.ADD_COUNT,
num,
});
export const subCountAction = (num) => ({
type: actionTypes.SUB_COUNT,
num,
});
export const changeBannerAction = (banner) => ({
type: actionTypes.CHANGE_BANNER,
banner,
});
./store/constants.js
export const ADD_COUNT = "add_count";
export const SUB_COUNT = "sub_count";
// App.jsx
import React,{ PureComponent } from "react";
import "./App.css";
import Detail from "./pages/detail";
import Home from "./pages/home";
import store from "./store";
export class App extends PureComponent {
constructor(){
super();
this.state = {
// 初始化count的时候,读取state中的count
count: store.getState().count
}
}
componentDidMount(){
// 组件挂载的时候订阅store,监听state的变化
// 可以在组件卸载的时候,取消订阅
store.subscribe(()=>{
const state = store.getState();
this.setState({count:state.count});
})
}
render() {
const { count } = this.state;
return (
<div>
<h1>App Count: {count}</h1>
<div className="pages">
<Home />
<Detail />
</div>
</div>
);
}
}
export default App
// ./pages/home.jsx
import React, { PureComponent } from "react";
import store from "../store";
import { addCountAction } from "../store/actionCreators";
export class Home extends PureComponent {
constructor() {
super();
this.state = {
count: store.getState().count,
};
}
componentDidMount() {
// 订阅state的变化
store.subscribe(() => {
const state = store.getState();
this.setState({ count: state.count });
});
}
addcount(num){
// 派发action,修改state
store.dispatch(addCountAction(num))
}
render() {
const { count } = this.state;
return (
<div>
<h2>Home Count: {count}</h2>
<button onClick={e=>this.addcount(1)}>+1</button>
<button onClick={e=>this.addcount(5)}>+5</button>
</div>
);
}
}
export default Home;
// ./pages/detail.jsx
import React, { PureComponent } from "react";
import store from "../store";
import { subCountAction } from "../store/actionCreators";
export class Detail extends PureComponent {
constructor() {
super();
this.state = {
count: store.getState().count,
};
}
componentDidMount() {
// 初始化的时候没有展示store中的数据
store.subscribe(() => {
const state = store.getState();
this.setState({ count: state.count });
});
}
subCount(num){
store.dispatch(subCountAction(num))
}
render() {
const { count } = this.state;
return (
<div>
<h2>Detail Count: {count}</h2>
<button onClick={()=>this.subCount(1)}>-1</button>
<button onClick={()=>this.subCount(8)}>-8</button>
</div>
);
}
}
export default Detail;
分析上面的代码,我们发现三个组件中有很多重复的逻辑部分。
但是核心的主要是两个:
- 在componentDidMount中订阅数据的变化,当数据发生变化的时候重置count;
- 在发生点击事件的时候,调用store.dispatch来派发对应的action;
这里我想是不是能自定义一个高阶组件,把相同代码逻辑抽取到高阶组件中。
// ./hoc/getStore.js
import React, { PureComponent } from "react";
import store from "../store";
import { addCountAction, subCountAction } from "../store/actionCreators";
function getStore (WrapperComponent) {
class NewComponent extends PureComponent {
constructor(){
super();
this.state = {
count: store.getState().count, // 获取初始化state
};
this.unSubScribe = null;
}
componentDidMount() {
// 在组件挂载的时候订阅state
this.unSubscribe = store.subscribe(() => {
const state = store.getState();
// 当count发生变化,更新组件内部的count
this.setState({ count: state.count });
});
}
componentWillUnmount() {
// 在组建卸载的时候,取消订阅
this.unSubscribe();
}
// 点击事件处理函数
handleClick(num, isAdd) {
if (isAdd) {
// 派发action
store.dispatch(addCountAction(num))
} else{
store.dispatch(subCountAction(num))
}
}
render() {
return (
<WrapperComponent
{...this.props}
{...this.state}
changeCountClick={(num,isAdd) => this.handleClick(num,isAdd)}
/>
);
}
}
return NewComponent;
}
// 导出高阶组件
export default getStore;
然后组件中可以直接调用高阶组件:
// ./pages/test.jsx
import React, { PureComponent } from "react";
import getStore from "../hoc/getStore";
export class Test extends PureComponent {
// 点击事件的处理方法
changeCount(num,isAdd){
this.props.changeCountClick(num,isAdd)
}
render() {
const { count } = this.props;
return (
<div>
<h2>Test Count:{count}</h2>
<button onClick={()=>this.changeCount(3,true)}>+3</button>
<button onClick={()=>this.changeCount(5,false)}>-5</button>
</div>
);
}
}
export default getStore(Test);
但其实我们可以不用自定义,有现成的库帮助解决,下面介绍一个很好用的库。
react-redux的使用
redux为了和react结合的更好,redux官方提供了react-redux的库,可以在直接在项目中使用。
安装react-redux:
npm install react-redux
由于应用中的任何react组件都可以连接到store,因此在顶层用一个<Provider>,将整个应用的组件树包裹起来。
// index.js
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import { Provider } from "react-redux";
import store from "./store";
const root = ReactDOM.createRoot(document.getElementById("root"));
// 不用每一个组件都引入一次store
root.render(
<Provider store={store}>
<App />
</Provider>
);
connect()函数将react组件连接到store,它像连接的组件提供其需要从store中获取的数据片段,以及可以用来向 store dispatch actions的功能。
connent()接受四种不同的、皆可选的参数;
- mapStateToProps?:Function
- mapDispatchToProps?:Function | Object
- mergeProps?:Function
- options?:Object
mapStateToProps 和 mapDispatchToProps 分别处理 Redux store 的 state 和 dispatch。state 和 dispatch 将作为第一个参数提供给 mapStateToProps 或 mapDispatchToProps 函数。
// ./pages/about.jsx
import React, { PureComponent } from "react";
import { connect } from "react-redux";
import { addCountAction, subCountAction } from "../store/counter";
export class About extends PureComponent {
calcCount(num, isAdd) {
if (isAdd) {
// 调用映射进props的函数
this.props.addCount(num);
} else {
this.props.subCount(num);
}
}
render() {
const { count } = this.props;
return (
<div>
<h2>About Count:{count}</h2>
<button onClick={() => this.calcCount(6, true)}>+6</button>
<button onClick={() => this.calcCount(6, false)}>-4</button>
</div>
);
}
}
const mapStateToProps = (state) => ({
count: state.count,
});
const mapDispatchToProps = (dispatch) => ({
addCount: (num) => {
// addCount是一个函数
dispatch(addCountAction(num));
},
subCount: (num) => {
// 派发action
dispatch(subCountAction(num));
},
});
// connect本身是一个高阶函数,接受两个函数作为参数,映射到props中
// connect返回值是一个高阶组件;
export default connect(mapStateToProps, mapDispatchToProps)(About);
异步操作
组件中的异步操作
前面的案例中,redux中保存的count是一个本地定义的数据,我们直接通过同步的操作来dispatch action,state就会被立即更新。
但是在真实开发中,redux中保存的很多数据可能来自服务器,我们需要进行异步的请求操作。现在我们来看看,在组件中发起请求获取数据的操作。
// ./pages/category.jsx
import React, { PureComponent } from 'react'
import axios from "axios";
import { connect } from "react-redux";
import { changeBannerAction } from '../store/actionCreators';
export class Category extends PureComponent {
// 组件加载
componentDidMount(){
// 在组件挂载的时候向服务器发起请求
axios.get("http://XXXX").then(res=>{
const banner = res.data.data;
// 调用函数,派发action
this.props.changeBanner(banner);
})
}
render() {
return (
<div>Category</div>
)
}
}
const mapDispatchToProps =(dispatch)=>({
changeBanner:(data)=>{
dispatch(changeBannerAction(data))
}
})
export default connect(null,mapDispatchToProps)(Category)
// ./store/actionCreators.js
// ..........
export const changeBannerAction = (banner) => ({
type: actionTypes.CHANGE_BANNER,
banner,
});
// ./store/reducer.js
const initialState = {
// .....
banner: []
};
function reducer(state = initialState, action) {
switch (action.type) {
// .....
case CHANGE_BANNER:
return { ...state, banner: action.banner };
default:
return state;
}
}
流程如下:
上面的代码有一个缺陷:我们必须将网络请求的异步代码放到组件的生命周期中来完成;
事实上,网络请求到的数据也属于我们状态管理的一部分,更好的方式应该是将它交给redux管理;
redux中的异步操作
我们之前创建的action都是返回一个对象,试图通过返回函数来发起网络请求。但是redux不允许这样操作。
那在redux中如何可以进行异步的操作呢?
答案是使用中间件。
官方推荐使用redux-thunk;
使用redux-thunk
安装:
npm install redux-thunk
在创建store时传入应用了中间件的enhance函数
- 通过applyMiddleware来结合多个中间件,返回一个enhancer;
- 将enhancer作为第二个参数传入到createStore()中
定义一个返回函数的action:
export const fetchHomeMultidataAction = () => {
// 如果是一个普通的action,那我们要返回一个对象
// 问题:对象不能直接从服务器拿数据啊
return function(dispatch, getState) { //函数被调用的时候,会传给这个函数一个dispatch和getState函数
axios.get("XXXX").then((res) => {
const banner = res.data.data;
dispatch(changeBannerAction(banner))
});
}
};
应用redux-thunk中间件:
import {createStore,applyMiddleware } from "redux";
import reducer from "./reducer";
import thunk from "redux-thunk";
const store = createStore(reducer,applyMiddleware(thunk));
// 用thunk对store进行增强,可以派发一个函数
export default store;
总结:rudex-thunk是怎么做到允许发送异步请求的呢?**
-
默认情况下的dispatch(action),action是一个js的对象;
-
redux-thunk可以让dispatch(action函数);
-
该函数会被调用,并且传给这个函数一个dispatch和getState函数;
- dispatch函数用于我们之后再次派发action
- getState函数用于获取之前一些状态
流程图:
redux文件拆分
随着应用程序的模块越来越多,各个业务模块的reducer和action在一个文件中,不利于后续的维护。所以实际开发中,我们可以把各个模块的reducer单独封装在一起。
redux给我们提供了一个combineReducer函数,来对多个reducer进行合并:
// index.js
import {createStore,applyMiddleware,combineReducers } from "redux";
import thunk from "redux-thunk";
import homeReducer from "./home";
import countReducer from "./counter";
const reducer = combineReducers({ // 合并多个reducer
counter:countReducer,
home: homeReducer
});
const store = createStore(reducer,applyMiddleware(thunk));
export default store;
以上全部,谢谢!