我正在参加「掘金·启航计划」
简单来说,react-redux用于将store与React中的页面联系在一起,它可以让 React 组件访问 state 片段 以及 通过派发(dispatch) actions 来更新 store 中所存储的数据,从而同 Redux 集成起来。
在学习react-redux之前最好先学习一下Redux的基本使用,参考文章:Redux的基本使用
一、为什么需要react-redux
在上篇文章Redux基本使用中我们可以知道,如果在React页面中使用Redux,凡是参与共享数据获取与修改的页面都要编写以下逻辑(以类组件为例)
- 引入创建好的store,并获取其中的数据作为初始数据
- 在组件挂载后订阅store中state的变化,一旦发生变化去执行setState方法从而让组件重新render
- 在修改共享数据的方法中去执行dispatch,其中传入对应的action
- 在组件卸载时去取消订阅
而这一系列操作是十分繁琐的,但是由于这一系列操作中有很多逻辑是十分相似的,所以可以将重复的逻辑抽取到一个高阶组件中(关于高阶组件的文章详见:React中的高阶组件与其使用场景)
在高阶组件中可以将传入的组件拦截起来去添加一系列逻辑,而react-redux库中就给我们提供了这样的一个高阶组件去将页面与store去连接在一起帮助我们做了一系列重复性的操作(这样就不需要在页面中额外去编写了)
二、react-redux的基本使用
2-0、搭建好环境
在学习react-redux之前先搭建好环境
- 首先通过脚手架去创建好一个react项目
create-react-app xxx
- 安装redux库
npm install redux
- 然后安装react-redux库
npm install react-redux
- 准备好store
在store/constants.js中定义好关于action type的常量
export const ADD_COUNT = "add_count";
export const SUB_COUNT = "sub_count";
在store/createActions.js中定义好生成action对象的方法
import * as actionTypes from "./constants";
export const addCountAction = (count) => ({
type: actionTypes.ADD_COUNT,
count,
});
export const subCountAction = (count) => ({
type: actionTypes.SUB_COUNT,
count,
});
在store/reducer.js中准备好创建store所需的reducer
import * as actionTypes from "./constants";
const initialState = {
count: 100,
};
function reducer(state = initialState, action) {
switch (action.type) {
case actionTypes.ADD_COUNT:
return { ...state, count: state.count + action.count };
case actionTypes.SUB_COUNT:
return { ...state, count: state.count - action.count };
default:
return state;
}
}
export default reducer;
在store/index.js中创建好store并导出方便后续使用
import { createStore } from "redux";
import reducer from "./reducer";
const store = createStore(reducer);
export default store;
2-1、获取store中的共享数据
2-1-1、使用Provider
使用react-redux中的Provider来为组件提供创建好的store
- 其底层实际上是Context
- 在Context的基础上做了一层封装,不是通过value来传递store,它提供了store属性用于传递store
src/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"));
root.render(
<React.StrictMode>
<Provider store={store}>
<App/>
</Provider>
</React.StrictMode>
);
这样之后就不用在需要使用共享数据的页面中都要进行import store了,直接使用Provider提供好的store即可
2-1-2、使用connect
使用react-redux库中提供的connect将store与页面连接在一起
- connect本身是一个函数,其返回值是一个高阶组件
- 而我们知道高阶组件本质上也是一个函数,所以在使用connect时,要通过connect(...)(页面组件)的方式去使用
也就是说connect函数去接收参数,返回一个函数(高阶组件),那么connect函数中接收的参数是干什么的呢?
connect函数中的第一个参数
在connect函数中接收两个参数,我们先了解一下第一个参数:
第一个参数是一个函数,其作用是:告诉高阶组件要将store中的哪些数据映射进去
- 因为store中存储的数据有很多,而不同页面中可能需要获取store中不同的数据(比如A页面需要state中的count,B页面需要state中的name...)
- 如果不管页面中用到了哪些共享数据都会把所有共享数据映射进去的话,一旦其中任何数据发生了变化,不管这个页面有没有用到都会重新进行render,所以这个参数就是让高阶组件明确知道该页面需要哪些数据,这样就能够保证在该页面使用到的数据发生变化时才进行render
该函数在被调用时会接收到传入进来的state,我们需要返回一个对象,在该对象中写入需要映射进来的数据即可
src/pages/Home.jsx
import React, { PureComponent } from 'react'
import { connect } from 'react-redux'
export class Home extends PureComponent {
render() {
const { count } = this.props
return (
<div>Home Count:{ count }</div>
)
}
}
// function mapStateToProps(state) {
// return {
// count: state.count
// }
// }
// 可以简化为
const mapStateToProps = (state) => ({count: state.count})
export default connect(mapStateToProps)(Home)
实际上,在传递好告知高阶组件映射哪些数据的函数后,高阶组件内部会将Home中的原始的props与传入函数中返回的对象进行合并,全部以props的形式传递给Home组件,相当于:
<Home {...this.props} {...obj} />
- obj为传递的mapStateToProps函数中返回的对象
所以在组件Home中可以在props中获取到映射进去的的共享数据
此时在App.jsx中使用Home.jsx组件后便可以看到获取到的共享数据count:
src/App.jsx
import React, { PureComponent } from 'react'
import Home from './pages/Home'
export class App extends PureComponent {
render() {
return (
<div>
<Home />
</div>
)
}
}
export default App
2-2、修改store中共享的数据
在使用react-redux之前,需要在组件中通过store.dispatch(action)去修改共享的数据,这样组件中的逻辑会与修改store中共享数据的逻辑耦合在一起。
而使用react-redux后,便可以实现组件逻辑与修改store中共享数据逻辑的解耦。 那么如何使用react-redux实现修改共享数据呢?
connect函数中的第二个参数
这就需要通过向connect中传递第二个参数来实现了:
connect中的第二个参数也是一个函数,其接受dispatch,返回一个对象
- 该对象中包含一个个的函数
- 该对象中包含的函数最终也会被映射到组件的props中
- 通过在这些函数中调取dispatch(action)来执行不同的操作
src/pages/Home.jsx
import React, { PureComponent } from 'react'
import { connect } from 'react-redux'
import { addCountAction, subCountAction} from '../store/createActions'
export class Home extends PureComponent {
addCount(count) {
this.props.addCount(count)
}
subCount(count) {
this.props.subCount(count)
}
render() {
const { count } = this.props
return (
<div>
Home Count:{ count }
<div>
<button onClick={() => this.addCount(10)}>+10</button>
<button onClick={() => this.subCount(10)}>-10</button>
</div>
</div>
)
}
}
// function mapStateToProps(state) {
// return {
// count: state.count
// }
// }
// 可以简化为
const mapStateToProps = (state) => ({count: state.count})
// function mapDispatchToProps(dispatch) {
// return {
// addCount(count) {
// dispatch(addCountAction(count))
// },
// subCount(count) {
// dispatch(subCountAction(count))
// }
// }
// }
// 可简化为
const mapDispatchToProps = (dispatch) => ({
addCount: (count) => dispatch(addCountAction(count)),
subCount: (count) => dispatch(subCountAction(count))
})
export default connect(mapStateToProps, mapDispatchToProps)(Home)
三、如何请求异步数据存储到store中
在业务开发中,我们需要共享的数据往往是获取的服务端的数据,是异步的,那么我们获取异步数据存储到store中呢?
方式一 在组件的生命周期种进行处理
以类组件为例,我们可以在componentDidMount生命周期中调取接口去获取数据,在then方法中通过connect中绑定到props中的修改共享数据方法去修改共享数据即可
比如,Home组件中需要展示轮播图,现在需要去获取服务端存取的banners数据
import React, { PureComponent } from 'react'
import { connect } from 'react-redux'
import { changeBannersAction } from '../store/createActions'
import axios from 'axios'
export class Home extends PureComponent {
componentDidMount() {
axios.get("url").then(res => {
const banners = res.data.data.banner.list
this.props.changeBanners(banners)
})
}
render() {
const { banners } = this.props
return (
<div>
<h2>轮播图展示</h2>
<ul>
{
banners.map((item, index) => {
return <li key={index}>{item.title}</li>
})
}
</ul>
</div>
)
}
}
const mapStateToProps = (state) => ({
banners: state.banners
})
const mapDispatchToProps = (dispatch) => ({
changeBanners: (banners) => dispatch(changeBannersAction(banners))
})
export default connect(mapStateToProps, mapDispatchToProps)(Home)
方式二 使用redux-thunk在redux中进行管理
在方式一中,我们将网络请求的异步代码放在了Home.jsx组件中,交由某个组件去管理,这种方式是不合理的。事实上网络请求到的数据属于状态管理的一部分,因为获取到的数据最终是存储在store中的,最好将它交给redux来管理。
正确的流程应该是下图所示:
也就是说网络请求的操作应该放到Redux中去处理,但是之前diaptch action后会同步地去执行reducer中的逻辑,我们该如何使得diaptch action中可以发送异步网络请求,并且在获取到结果后再去执行reducer中的逻辑呢?
如果是需要获取异步数据,那么action就不能再是一个对象了,因为我们无法知道该怎样返回这个对象,此时数据是未知的。
如果想通过异步的方式去派发action则需要action是一个函数,因为只有返回一个函数才能在该函数中进行一些异步操作,然而redux是不支持dispatch中传入一个函数的,如果使用dispatch派发一个函数,会发现报以下错误:
该错误告诉我们如果想要dispatch一个函数需要添加一个中间件:redux-thunk
那么redux-thunk如何使用呢?
1. 安装redux-thunk库
npm install redux-thunk
2. 在store上使用中间件
在createStore中可以传递第二个参数,第二个参数可以使用redux中提供的applyMiddleware方法并传入想使用的中间件来告知store使用哪些中间件
- applyMiddleware中可以接收多个参数,若想使用多个中间件依次传入即可
src/store/index.js
import { createStore, applyMiddleware } from "redux";
import thunk from "redux-thunk";
import reducer from "./reducer";
const store = createStore(reducer, applyMiddleware(thunk));
export default store;
此时,便可以向dispatch中传递一个函数,redux-thunk内部发现dispatch一个函数的时候会自动帮我们去执行这个函数。
3. 编写action中的异步处理逻辑
redux-thunk内部在执行dispatch中函数的时候会帮我们传递进来两个参数,分别是:
- 参数一
dispatch
(通过dispatch(action对象)来实现获取异步数据成功后的派发逻辑)- 参数二
getState
(通过getState().xxx可获取其中共享的数据)
src/store/createActions.js
import * as actionTypes from "./constants";
import axios from "axios";
export const changeBannersAction = (banners) => ({
type: actionTypes.CHNAGE_BANNERS,
banners,
});
export const fetchHomeBannersAction = () => {
return function (dispatch) {
axios.get("url").then((res) => {
const banners = res.data.data.banner.list;
dispatch(changeBannersAction(banners));
});
};
};
- 在Home.jsx中去通过connect映射state与dispatch实现相关逻辑
src/pages/Home.jsx
import React, { PureComponent } from 'react'
import { connect } from 'react-redux'
import { fetchHomeBannersAction } from '../store/createActions'
export class Home extends PureComponent {
componentDidMount() {
this.props.fetchHomeBanners()
}
render() {
const { banners } = this.props
return (
<div>
<h2>轮播图展示</h2>
<ul>
{
banners.map((item, index) => {
return <li key={index}>{item.title}</li>
})
}
</ul>
</div>
)
}
}
const mapStateToProps = (state) => ({
banners: state.banners
})
const mapDispatchToProps = (dispatch) => ({
fetchHomeBanners: () => dispatch(fetchHomeBannersAction())
})
export default connect(mapStateToProps, mapDispatchToProps)(Home)
四、扩展-reducer模块拆分
在实际的业务开发中,store种管理的数据有很多,而且不同页面使用的共享数据不同,如果将处理这些共享数据的逻辑都放在同一个reducer中会让代码变得难以维护。如果是多人开发的情况下这样会让代码更加混乱。
所以我们可以考虑对reducer进行拆分:
比如将A页面中使用的count数据用一个reducer进行处理;B页面中使用的banners数据用另一个reducer来处理
使得每个模块都有自己的constants、createActions、以及reducer处理逻辑
拆分之后逻辑便方便维护了,那么在index中创建store时,如何将拆分的各个reducer进行合并呢?
此时就需要redux中提供的combineReducers来实现多个reducer的合并了:
src/store/index:
import { createStore, applyMiddleware, combineReducers } from "redux";
import thunk from "redux-thunk";
import countReducer from "./count/reducer";
import bannerReducer from "./banner/reducer";
const reducer = combineReducers({
count: countReducer,
banner: bannerReducer,
});
const store = createStore(reducer, applyMiddleware(thunk));
export default store;
需要注意的是,通过combineReducers进行合并后,state中的数据将被差分到不同模块中
- 合并时传入的对象中的key即模块名
- 如果此时想获取其中的数据比如count,那么需要通过store.getState().count.count来获取
- 在页面中映射state中数据的时候也需要注意,此时就不能通过state.xx来获取想要映射的数据了,要通过state.模块名.数据来映射