React系列(七)——Redux的使用
前言
当我们的前端项目功能点比较多,组件关系比较复杂时,单纯的使用React原生的方法进行组件数据的传递实现起来可能相对比较麻烦,在这种场景下我们可能会需要有一个可以帮助我们做全局状态管理的库来解决这个问题。
Redux
是一个专门用于做状态管理的JS库(不是react插件库),他可以集中式管理React应用中多个组件共享的状态。本篇文章将对redux
以及react-redux
的使用和功能进行讲解,希望对各位读者有所帮助。
一、先来了解Redux
(一)为什么要使用Redux
在讲Redux的使用之前,我们不妨先回忆一下之前我们实现组件通信的方式都有哪些?
对于逻辑简单的父子组件来说,我们一般会直接使用props
参数来传递组件中的状态或者方法,而对于兄弟组件的通信,我们可以先把数据交由兄弟组件共有的父组件,再通过props
参数传递到对应的子组件。但这种做法较为麻烦,遇到嵌套层次比较深,需要传递的属性比较多时,实现起来往往会大费周章。目前主流的解决思路主要有两种,第一种是利用发布订阅模式,组件间使用第三方库进行通信,常见的有库有PubSubJS
,第二种解决思路就是将组件的状态交由一个对象全局进行管理,任何组件都可以通过这个全局对象来获取其他组件的值。
Redux
就是第二种解决思路的解决方案,redux
是一个专门用于做状态管理的JS库(不是react插件库)。它可以用在react
, angular
, vue
等项目中, 但基本与react
配合使用。
(二)Redux的学习文档
Redux的学习资料还是蛮多的,一般来说我们可以去中文的官方文档去查看对应的API以及其他特性的使用:
- 英文文档: https://redux.js.org/
- 中文文档: http://www.redux.org.cn/
- Github: https://github.com/reactjs/redux
(三)Redux的工作过程
Redux的工作原理图对于Redux的学习,其实只要掌握了上面的工作流程图,那么Redux的核心就掌握得差不多了。原理图中一共有4个对象,ReactComponent指的是我们自定义的组件,而ActionCreators
、Store
、Reducers
则是Redux中提出的新概念。
ActionCreator:用于创建action对象,可以返回我们具体要对组件共享状态进行哪些操作,比如说Redux帮助我们维护了A组件的count数据,那么如果我们现在想要对count进行+1的操作的话,我们就需要actionCreator帮助我们生成一个type
为add,data为1的对象。
Store:store是Redux中的核心,负责管理state状态和调度Reducer
,当我们把action交给store之后,store并不会直接对状态进行修改,而是交给对应的reducer来对状态进行更新。
Reducer:reducer用于初始化状态和加工状态,当store指定reducer进行更新状态时,reducer会根据原有的state和action进行加工,返回新的state。
二、使用Redux来进行状态管理
(一)使用Redux来实现一个小需求
我们先来看这样一个小案例:组件中有4个按钮,点击后分别会对取下拉框的值对state中的值count进行加
、减
、奇数加
、异步加
的操作。
export default class Count extends Component {
state = {count:0}
//加法
increment = ()=>{
const {value} = this.selectNumber
const {count} = this.state
this.setState({count:count+value*1})
}
//减法
decrement = ()=>{
const {value} = this.selectNumber
const {count} = this.state
this.setState({count:count-value*1})
}
//奇数再加
incrementIfOdd = ()=>{
const {value} = this.selectNumber
const {count} = this.state
if(count % 2 !== 0){
this.setState({count:count+value*1})
}
}
//异步加
incrementAsync = ()=>{
const {value} = this.selectNumber
const {count} = this.state
setTimeout(()=>{
this.setState({count:count+value*1})
},500)
}
render() {
return (
<div>
<h1>当前求和为:{this.state.count}</h1>
<select ref={c => this.selectNumber = c}>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
</select>
<button onClick={this.increment}>+</button>
<button onClick={this.decrement}>-</button>
<button onClick={this.incrementIfOdd}>当前求和为奇数再加</button>
<button onClick={this.incrementAsync}>异步加</button>
</div>
)
}
}
上面这种方式是通过纯react的方式来实现的,下面我们换成使用redux来实现:
步骤一:安装redux
npm install --save redux
步骤二:定义专门用于处理count状态的reducer
关于这一步,需要对一个小细节做一下解释,当store分发对应的动作给reducer时,reducer一共会收到2个参数,分别是preState和action,前者可以理解为当前count的值,action即为即将对count进行什么操作。但是在状态初始化时,此时由于count尚未存在,所以此时preState
就会为undefined
,而action将会是redux自动帮我们封装好的一个action对象,type为类似@@initxxxx
这样的字符串,value为空。所以我们需要在countReducer中针对初始化这种情况,给count的初始值进行赋值。
可以参考下面这种方式,嫌麻烦的话也可以直接在default
中返回对应的初始化值就行,比如这个案例就可以直接返回0。
const initState = 0 //初始化状态
export default function countReducer(preState=initState,action){
// console.log(preState);
//从action对象中获取:type、data
const {type,data} = action
//根据type决定如何加工数据
switch (type) {
case 'increment': //如果是加
return preState + data
case 'decrement': //若果是减
return preState - data
default:
return preState
}
}
步骤三:定义store.js
文件,暴露store对象
这里的话,使用了redux的核心APIcreateStore
来创建store,传入的参数是具体的reducer
import {createStore} from 'redux'
//引入为Count组件服务的reducer
import countReducer from './count_reducer'
//暴露store
export default createStore(countReducer)
步骤四:定义count状态对应的actionCreator
export const createIncrementAction = data => ({type: 'increment',data})
export const createDecrementAction = data => ({type: 'decrement',data})
步骤五:在Count组件中进行共享状态的获取和修改
这里的话,我们使用了store.getState()
来获取count当前的状态,在具体修改状态的方法中使用了store.dispatch()
来分发对应的action。
import React, { Component } from 'react'
//引入store,用于获取redux中保存状态
import store from '../../redux/store'
//引入actionCreator,专门用于创建action对象
import {createIncrementAction,createDecrementAction} from '../../redux/count_action'
export default class Count extends Component {
state = {carName:'奔驰c63'}
//加法
increment = ()=>{
const {value} = this.selectNumber
store.dispatch(createIncrementAction(value*1))
}
//减法
decrement = ()=>{
const {value} = this.selectNumber
store.dispatch(createDecrementAction(value*1))
}
//奇数再加
incrementIfOdd = ()=>{
const {value} = this.selectNumber
const count = store.getState()
if(count % 2 !== 0){
store.dispatch(createIncrementAction(value*1))
}
}
//异步加
incrementAsync = ()=>{
const {value} = this.selectNumber
setTimeout(()=>{
store.dispatch(createIncrementAction(value*1))
},500)
}
render() {
return (
<div>
<h1>当前求和为:{store.getState()}</h1>
<select ref={c => this.selectNumber = c}>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
</select>
<button onClick={this.increment}>+</button>
<button onClick={this.decrement}>-</button>
<button onClick={this.incrementIfOdd}>当前求和为奇数再加</button>
<button onClick={this.incrementAsync}>异步加</button>
</div>
)
}
}
通过上面这种方式,我们就可以实现把Count组件的count属性交由Redux来进行管理了。但此时还存在一个问题:Redux虽然已经帮我们进行了状态的管理,可是当状态的值发生变更时,redux并不会帮我们刷新页面。所以常见的我们有两种方式来解决这个问题:
方式一:在组件的componentDidMount
钩子中,定义刷新时机
下面的写法表示,每当store发生变更,组件就会空调用一次setState()
,从而让组件执行render方法
componentDidMount(){
store.subscribe(()=>{
this.setState({})
})
}
方式二(常用):在最外层的index.js
中为组件绑定更新动作
由于App组件是所有组件的父组件,一旦store中的状态发生更新,
ReactDOM.render(<App/>,document.getElementById('root'))
store.subscribe(()=>{
ReactDOM.render(<App/>,document.getElementById('root'))
})
(二)引入constant.js
文件进行优化
上面的代码虽然实现了Redux的状态管理,但在实际使用中,我们会对action中的type进行统一的常量封装,最终维护到一个constant.js
文件中
步骤一:定义constant.js
文件
/*
该模块是用于定义action对象中type类型的常量值,目的只有一个:便于管理的同时防止程序员单词写错
*/
export const INCREMENT = 'increment'
export const DECREMENT = 'decrement'
步骤二:在count_reducer.js
中引入constant.js
/*
1.该文件是用于创建一个为Count组件服务的reducer,reducer的本质就是一个函数
2.reducer函数会接到两个参数,分别为:之前的状态(preState),动作对象(action)
*/
import {INCREMENT,DECREMENT} from './constant'
const initState = 0 //初始化状态
export default function countReducer(preState=initState,action){
// console.log(preState);
//从action对象中获取:type、data
const {type,data} = action
//根据type决定如何加工数据
switch (type) {
case INCREMENT: //如果是加
return preState + data
case DECREMENT: //若果是减
return preState - data
default:
return preState
}
}
步骤三:在count_action.js
文件中引入constant.js
/*
该文件专门为Count组件生成action对象
*/
import {INCREMENT,DECREMENT} from './constant'
//同步action,就是指action的值为Object类型的一般对象
export const createIncrementAction = data => ({type:INCREMENT,data})
export const createDecrementAction = data => ({type:DECREMENT,data})
//异步action,就是指action的值为函数,异步action中一般都会调用同步action,异步action不是必须要用的。
export const createIncrementAsyncAction = (data,time) => {
return (dispatch)=>{
setTimeout(()=>{
dispatch(createIncrementAction(data))
},time)
}
}
表面看抽取的constant.js
文件似乎反而增加了使用的复杂度,但实际上当变量比较多时,使用常量这种方式可以减少开发人员由于单词拼写错误导致的状态更新失败等问题。
三、异步action的定义
在之前的案例中,我们传递的action对象都是一般对象(plain Object)。但实际上,action还可以有另外一种类型: 异步action(其实也可以理解为是函数action),也就是说我们对状态的操作时放在异步的任务中完成,同时,这个异步的任务不是组件自己来实现,而是actionCreator来实现。需要注意的是
(1) 如果传递的action对象是函数,那么我们需要使用 redux-thunk 来对store进行配置.
(2) 当store调用dispatch方法的时候,如果发现传入的参数是函数,那么store会帮我们调用这段函数,并且传递给我们一个dispatch对象,供我们返回plain Object对象的时候直接调用。
上一小节的案例中,有一个按钮的功能是异步加,异步的动作我们是在组件的方法中直接调用的,在本小节中,我们将通过异步action来进行实现:
步骤一:引入redux-thunk
store默认只支持一般对象,如果想要让store支持函数的话,就需要借助redux-thunk
来进行调和。
npm i redux-thunk
步骤二:在store.js
中引用redux-thunk
这里的话,需要在redux中导入一个新的函数:applyMiddleware,帮助store支持中间件
/**
* 这个js文件主要是用于对外暴露一个store对象
* store对象中需要传入具体的reducer对象,reducer对象中定义了具体怎么操作数据的方法
*/
import {createStore, applyMiddleware} from 'redux'
import countReducer from './count_reducer'
// 引入 redux-thunk 用于支持异步action
import thunk from 'redux-thunk'
export default createStore(countReducer,applyMiddleware(thunk));
步骤三:在Count组件对应的actionCreatoe中定义一个异步action
我们可以看到,actionCreator一般的返回值是对象,我们这里的返回值是一个函数,默认可以接收到dispatch
对象,同时我们在函数中进行了异步(设置定时器)的操作。
// 异步action
export const createAsyncIncrementAction = (data,time) => {
return (dispatch)=>{
setTimeout(()=>{
dispatch(createIncrementAction(data));
},time)
}
}
步骤四:在自定义组件中使用异步action
import React, { Component } from 'react';
import store from '../../redux/store';
import {createDecrementAction,createIncrementAction,createAsyncIncrementAction} from '../../redux/count_action'
export default class Count extends Component {
incrementAsync = () => {
const { value } = this.countNum;
store.dispatch(createAsyncIncrementAction(value*1,500));
}
render() {
return (
<div>
<h2>总数为: {store.getState()}</h2>
<select ref={a => this.countNum = a}>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
</select>
...
<button onClick={this.incrementAsync}>add async</button>
</div>
)
}
}
我们可以看到,通过异步action我们就可以不需要自己在方法中手动写异步函数了
四、react-redux的使用
redux并不是react官方推出的状态共享库,但随着使用人数的增加,react官方在后期也推出了**react-redux**来方便开发者更好的使用redux来进行状态的管理。然后react-redux的使用和原先有些差别:
(1)提出了UI组件和容器组件的概念
(2)UI组件负责展示页面和数据,容器组件作为中间桥梁负责连接UI组件和redux之间的数据传递
react-redux虽然多了一些新概念,相比于原先使用纯粹的redux进行状态管理多了一定程度的复杂性,但是它也确实能够简化我们的一部分代码。具体都简化了哪些,我们不妨来看下面的实现步骤吧:
步骤一:下载react-redux
npm i react-redux
步骤二:定义容器组件
在已有UI组件的基础上(这里我们以上一小节的组件作为UI组件),我们新建一个和component
同级的container
目录,里面存放我们的容器组件。创建容器组件主要依靠的是react-redux
库中的connect
函数,connect
函数是一个高阶函数(返回值还是一个函数),第一次传入的参数是两个函数,分别对应传递给组件的状态和可供组件调整状态的方法,第二次传入的参数就比较固定了,就是我们的UI组件。
import {connect} from 'react-redux'
import CountUI from '../../components/Count'
import {createIncrementAction,createDecrementAction,createAsyncIncrementAction} from '../../redux/count_action'
/**
* 由于 UI组件并不能直接获取redux管理的状态,所以这里的话需要由
* 容器组价将状态以及操作状态的方法作为参数传递到connect()函数中
*/
// 该方法返回的UI组件所需要获取的redux管理的状态属性
function mapStateToProps(state){
return {count:state}
}
// 该方法返回的UI组件所需要的操作对应状态的方法
function mapDispatchToProps (dispatch){
return {
increment: number => dispatch(createIncrementAction(number)),
decrement: number => dispatch(createDecrementAction(number)),
incrementAsync: (number,time) => dispatch(createAsyncIncrementAction(number,time))
}
}
// 作为参数传入后,UI组件可以通过props属性读取对应的值
export default connect(mapStateToProps,mapDispatchToProps)(CountUI);
当然了,我们这里可以有更加简洁的写法:
(1)把UI组件和容器组件都放在同一个文件中
(2)把connect第一次调用的入参进行格式的优化,返回的action会由store自动帮助我们进行分发。
import { connect } from 'react-redux'
import { createIncrementAction, createDecrementAction, createAsyncIncrementAction } from '../../redux/count_action'
import React, { Component } from 'react';
class Count extends Component {
...
}
// 作为参数传入后,UI组件可以通过props属性读取对应的值
export default connect(
state => ({count: state }),
{
increment: createIncrementAction,
decrement: createDecrementAction,
incrementAsync: createAsyncIncrementAction
}
)(Count);
步骤三:调整UI组件中获取共享状态和更新状态方法的方式
其实这里的话,就是由原来的直接导入store
和actionCreator
来获取数据改为通过props
进行获取。
class Count extends Component {
// 有了actionCreator , 我们就不需要自己再去定义action了
increment = () => {
const { value } = this.countNum;
this.props.increment(value * 1)
}
decrement = () => {
const { value } = this.countNum;
this.props.decrement(value * 1)
}
incrementIfOdd = () => {
const { value } = this.countNum;
if (this.props.count % 2 === 1) {
this.props.increment(value * 1)
}
}
incrementAsync = () => {
const { value } = this.countNum;
this.props.incrementAsync(value * 1, 500)
}
render() {
return (
<div>
<h2>总数为: {this.props.count}</h2>
...
</div>
)
}
}
步骤四:在App组件中给Count组件传入store对象
组件有了store
对象,容器组件才可以获取到store中的state
以及dispatch
对象
import React,{Component} from 'react';
import Count from './containers/Count';
import store from './redux/store'
export default class App extends Component{
render(){
return (
<div>
<Count store={store}/>
</div>)
}
}
容器组件一旦数量多了,我们可能就不得不每个组件都要手动传递store
属性进去,这样比较麻烦,我们可以使用react-redux
中自带的<Provider>
组件来帮助我们简化这一步的操作。具体的步骤如下:
(1)在最外层的index.js中使用<Provider>
标签来包裹<App/>
标签
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import {Provider} from 'react-redux'
import store from './redux/store'
ReactDOM.render(
<Provider store={store}>
<App/>
</Provider>,document.getElementById('root'));
(2)原有的App.js
文件可以不用传递store属性了
import React,{Component} from 'react';
import Count from './containers/Count';
export default class App extends Component{
render(){
return (
<div>
<Count/>
</div>)
}
}
在这里,我们对使用react-redux
进行开发的优势和注意事项来做一个小结:
(1)react-redux
有着UI组件和容器组件的概念,UI组件并不能直接和redux进行沟通,而是要借助容器组件作为中间桥梁,获取和操作组件共享状态的方法都由容器组件来提供。这样虽然一定程度上增加了使用的复杂性,但是让组件的职责变得更加清晰。
(2)使用react-redux
可以自动实现状态和页面的联动刷新,让我们不需要给APP组件或者其他自定义组件进行store.subscribe()
的动作更新绑定了。
(3)针对react-redux有专门的开发者工具(浏览器插件)可以使用,帮我们更好地分析组件的状态。
五、store中存在多个共享状态的处理
在之前的案例中,无论是使用redux
还是react-redux
,我们都只是对store只保存一个状态的场景进行演示,但实际中稍微大一些的项目,基本上store中都是会存放多个状态值的(否则使用也就失去了使用redux的意义)。
实际上,当store只存储一个数值时,此时 store.state
的值就是一个number类型的数字,要想满足store可以存放多个数量、多种数据类型的状态,那么此时的store.state
就需要是Object类型的才可以满足。
假设我们现在在原有案例的基础上新增了一个Person组件,那么此时的store又应该做出什么调整呢?
步骤一:使用combineReducers
函数, 整合多个reducer,再作为参数传递给store
import {createStore, applyMiddleware,combineReducers} from 'redux';
// 引入 redux-thunk 用于支持异步action
import thunk from 'redux-thunk';
import countReducer from './reducers/count';
import personReducer from './reducers/person';
// 注意,如果是有多个状态需要保存,那么在一开始调用combineReducers的时候,就要设置好对象中各个状态的key
const allReducers = combineReducers({
count:countReducer,
persons:personReducer
})
export default createStore(allReducers,applyMiddleware(thunk));
步骤二:调整对应容器组件获取状态的方式
比如之前的Count组件就不再直接通过count = state
的方式来获取状态了,而是通过count = state.count
来获取。
export default connect(
state => ({count: state.count }),
{
increment: createIncrementAction,
decrement: createDecrementAction,
incrementAsync: createAsyncIncrementAction
}
)(Count);
需要注意的是,在实际开发中,为了让store.js
文件更加清晰,我们常常会将各个状态的reducer
单独抽取出来到一个文件中进行整合,再导入到store.js
文件中。
将汇总好的reducer再导入到store.js中
说在最后:
Redux虽然可以帮助我们更好地管理组件间共享的状态,但redux除了需要额外的学习成本之外,也一定程度上增加了项目的复杂性。如果只是小型项目,一般建议还是不引入Redux,直接使用原生react特性可能更为方便。
而react-redux
其实只是官方为了方便我们更好地使用redux而推出的一个集成库,实际上可用可不用,只是说react-redux
在某些方面也确实起到了简化部分开发工作的作用。所以在实际应用中,大家可以根据实际情况来使用。
本篇文章的详细案例代码,可以在我的码云上面下载:https://gitee.com/moutory/redux_test