Redux该怎么在React中使用呢?

前言

当业务逻辑越来越复杂的时候,你会发现使用React原生的state会是件很繁琐的事情,比如我的上一篇学习记录讲到的跨组件通信案例,管理各个组件的state就像蜘蛛丝一样牵连复杂,并且具有很高的风险,稍微不注意state就会发生预料不到的改变,这个时候我们可以用到强大且干净利落的状态管理工具Redux。

为什么要用到状态管理工具?

前端工程师在团队中肩负的任务越来越繁重,开发SPA单页面应用遇到错综复杂的需求也很常见,这时候就需要管理非常多的state,其中可能包括服务器的相应、缓存数据、本地生成尚未持久化到服务器的数据,UI的各种状态,当一个组件的state变化会影响到其他一个甚至多个组件state变化时,并且当多个路由下的组件需要相互通信时,这个时候仍然使用React自带的state来管理状态显然是一件头疼的事情,因此出现各种状态管理工具,Redux就是其中优秀工具之一。

Redux的三大原则

单一数据流

应用中的state都被存储在唯一的store中,所有数据一目了然,这给我们带来非常大的便利来调试应用,任何组件都可以访问到store的任何数据,这样我们就不需要操心各种跨组件通信,当产品经理改需求时,也不用让整个应用大动干戈,因为你只需要维护唯一的数据源store。

State只能读取

第一大原则中我们说到“任何组件都能访问到store的任何数据”,这是不是表示store就像全局变量一样危险?当然不是,因为store中的state是只读的,如果要修改state,都要严格按照Redux的API来执行,也就是action,action是一个用于描述事件将如何发生的对象,只有action才能改变store中的state,因此Redux让应用的状态改变变得“可预测”。

使用纯函数来修改state

单有action是无法改变state的,这时我们需要配合Reducer纯函数,Reducer存在的意义就是接受action的指令改变state并且返回新的state,它是纯函数,不会对应用产生任何的副作用。

简单demo

说了这么多,我们用code来举个例子🌰:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import { createStore } from 'redux';

/*
counter是一个reducer函数,它的作用是传入store中state的旧值和描述事件发生的action对象
*/
function counter(state = 0, action) {
switch (action.type) {
case 'INCREMENT':
return state + 1;
case 'DECREMENT':
return state - 1;
default:
return state;
}
}

// 创建 Redux store 来存放应用的状态。
// API 是 { subscribe, dispatch, getState }。
let store = createStore(counter);

// 监听state变化,当有变化时,触发subscribe中的函数
store.subscribe(() =>
// 使用store.getState()获取最新的state
console.log(store.getState())
);

// 改变内部 state 惟一方法是 dispatch 一个 action,将会触发subscribe中的函数
store.dispatch({ type: 'INCREMENT' });
// 1
store.dispatch({ type: 'INCREMENT' });
// 2
store.dispatch({ type: 'DECREMENT' });
// 1

这个例子涵盖了大部分Redux的API和使用方式,但是具体应用到React当中,我们还需进行一下步骤。

一个简单的React与Redux计数器demo

我们要了解一个事实,React与Redux到底是什么关系?其实这两者一点关系都没有,Redux是一个单独的状态管理工具,它不仅可以用在React中,还可以用到其他的前框架中,只不过因为React的“ state 函数的形式来描述界面”的特性,React与Redux成为一对较好的搭档。也就是为什么我们说Redux是React全家桶之一。

1、
用create-react-app初始化一个react项目,npm install redux --save,下载redux包。

2、创建index.redux.js文件,写入计数器需要的action对象、reducer函数,和action creator:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//  action对象,描述“触发动作”
const ADD_NUM = 'add num';
const REDUCE_NUM ='reduce num';

//reducer,接受旧的state(第一次为初始化state)和action,返回新的state

export function counter(state=0,action){
switch(action.type){
case ADD_NUM:
return state+1
case REDUCE_NUM:
return state-1
default:
return 10
}
}

//action creator 生成 action 的方法。“action” 和 “action 创建函数” 这两个概念很容易混在一起,使用时最好注意区分

export function addNum(){
return { type:ADD_NUM }
}
export function reduceNum(){
return { type: REDUCE_NUM }
}

3、修改index.js为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import React from "react";
import ReactDom from "react-dom";
import { createStore } from 'redux';
import App from "./App";
import { counter, addNum, reduceNum } from './index.redux';

//利用redux的createStore API创建store
const store = createStore(counter);

function render() {
//将redux组件传入App组件当中
ReactDom.render(<App store={store} addNum={addNum} reduceNum={reduceNum} />,document.getElementById('root'));
}
//初次渲染
render();

//利用store API监测当store中的state变化的时候自动启动subscribe监听函数,再次渲染render
store.subscribe(render);

4、修改App.js为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

import React from 'react';

class App extends React.Component{
render(){

//引入props下来的store和action
const store = this.props.store;
const num = store.getState(); //获取state值
const addNum = this.props.addNum;
const reduceNum = this.props.reduceNum;
return(
<div>
<h1>现在的数量为{num}</h1>
<button onClick={() => store.dispatch(addNum())} >+</button>
<button onClick={() => store.dispatch(reduceNum())} >-</button>
</div>
)
}
}

export default App

这样我们就完成一个简单的React与Redux结合的计数器例子在线查看我的例子(需翻墙:Codesandbox,也许你会问这么简单的逻辑为何要用这么复杂的方式来实现,因为这只是简单介绍Redux的用法,当需求超级复杂的时候,使用Redux带来的便利谁用谁知道~

异步Redux的使用方式

你会注意到上个例子中,如果要用异步函数,是否无从下手,这时候就需要用到redux-thunk这个插件来实现异步,同样我们以前一个计数器例子来实现。

1、安装npm install redux-thunk --save插件

2、修改index.js为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import React from "react";
import ReactDom from "react-dom";
//添加applyMiddleware,才能使用thunk这个中间件
import { createStore,applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import App from "./App";
import { counter, addNum, reduceNum, addNumAsync } from './index.redux';

//利用redux的createStore API创建store,并且使用thunk中间件
const store = createStore(counter, applyMiddleware(thunk));

function render() {
//将redux组件传入App组件当中
ReactDom.render(<App store={store} addNum={addNum} reduceNum={reduceNum} addNumAsync={addNumAsync} />,document.getElementById('root'));
}
//初次渲染
render();

//利用store API监测当store变化的时候自动启动subscribe监听函数
store.subscribe(render);

3、修改index.redux.js为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
const ADD_NUM = "add num";
const REDUCE_NUM = "reduce num";

//reducer

export function counter(state = 0, action) {
switch (action.type) {
case ADD_NUM:
return state + 1;
case REDUCE_NUM:
return state - 1;
default:
return 10;
}
}

//action creator

export function addNum() {
return { type: ADD_NUM };
}
export function reduceNum() {
return { type: REDUCE_NUM };
}

//这里特别注意的是addNumAsync返回一个setTimeout回调函数,第一个dispatch为参数,模拟异步addNum
export function addNumAsync() {
return dispatch => {
setTimeout(() => {
dispatch(addNum());
}, 2000);
};
}

修改App.js为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

import React from 'react';

class App extends React.Component{
render(){

//引入redux状态库
const store = this.props.store;
const num = store.getState();
const addNum = this.props.addNum;
const reduceNum = this.props.reduceNum;
//异步函数与其他函数一样使用
const addNumAsync = this.props.addNumAsync;
return(
<div>
<h1>现在的数量为{num}</h1>
<button onClick={() => store.dispatch(addNum())} >+</button>
<button onClick={() => store.dispatch(reduceNum())} >-</button>
<button onClick={() => store.dispatch(addNumAsync())} >+ Async</button>
</div>
)
}
}

export default App

使用异步时,我们只需要在applyMiddleware中加一个thunk即可实现,查看在线demo:Codesandbox

为React量身定制 的React-Redux

我们之前说过React与Redux并没有什么直接的关系,但是React-Redux就不一样了,React-Redux让React使用Redux变得更加简洁,可以理解为React-Redux是Redux专门为React定制的状态管理工具。

1、安装react-redux,npm install react-redux --save

2、修改index.js为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import React from "react";
import ReactDom from "react-dom";
import { createStore,applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import { Provider } from 'react-redux';
import App from "./App";
import { counter } from './index.redux';
//import { counter, addNum, reduceNum, addNumAsync } from './index.redux';

//利用redux的createStore API创建store
const store = createStore(counter, applyMiddleware(thunk));

//Provider包裹的组件能够使用store中的state和action,在Provider中我们只需要传入store,并且无需subscribe监听,因为react-redux都给我们做好了
ReactDom.render(
(
<Provider store={store} >
<App />
</Provider>
),
document.getElementById('root')
)

// 以下是未使用React-redux
//function render() {
//将redux组件传入App组件当中
// ReactDom.render(<App store={store} addNum={addNum} reduceNum={reduceNum} addNumAsync={addNumAsync} />,document.getElementById('root'));
//}
//初次渲染
//render();

//利用store API监测当store变化的时候自动启动subscribe监听函数
//store.subscribe(render);

3、修改App.js为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

import React from 'react';
import { connect } from 'react-redux';
import { addNum, reduceNum, addNumAsync } from './index.redux';

class App extends React.Component{
render(){
return(
<div>
<h1>现在的数量为{this.props.num}</h1>
<button onClick={this.props.addNum} >+</button>
<button onClick={this.props.reduceNum} >-</button>
<button onClick={this.props.addNumAsync } >+ Async</button>
</div>
)
}
}
//数据层
const mapStateProps = (state)=>{
return {num:state}
}
//改变数据的action
const actionCreators = { addNum, reduceNum, addNumAsync };
//将store和action注入到App组件
App = connect(mapStateProps, actionCreators)(App);
export default App

react-redux极大的简化了我们使用redux的步骤,查看我的在线demo:Codesandbox

使用装饰器优化React-Redux中的connect

在未使用ES7的装饰器新特性时,我们是这样使用connect的,看起来确实有些繁琐,改进方案也很简单。

1
2
3
4
5
6
7
8
9
//数据层
const mapStateProps = (state)=>{
return {num:state}
}
//改变数据的action
const actionCreators = { addNum, reduceNum, addNumAsync };
//将store和action注入到App组件
App = connect(mapStateProps, actionCreators)(App);
export default App

1、我们的项目是用creact-react-app初始化的,使用装饰器是需要配置package.json中的plugins,于是我们npm run eject呼出个性化配置,然后安装npm install babel-plugin-transform-decorators-lagacy插件。
2、打开package.json,在“babel”中添加"plugins":["transform-decorators-legacy"]

1
2
3
4
5
6
"babel": {
"presets": [
"react-app"
],
"plugins":["transform-decorators-legacy"]
},

3、修改App.js 为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

import React from 'react';
import { connect } from 'react-redux';
import { addNum, reduceNum, addNumAsync } from './index.redux';
//使用装饰器方式优化connect
@connect(
state=>({num:state}),
{ addNum, reduceNum, addNumAsync }
)
class App extends React.Component{
render(){
return(
<div>
<h1>现在的数量为{this.props.num}</h1>
<button onClick={this.props.addNum} >+</button>
<button onClick={this.props.reduceNum} >-</button>
<button onClick={this.props.addNumAsync} >+ Async</button>

</div>
)
}
}
// //数据层
// const mapStateProps = (state)=>{
// return {num:state}
// }
// //改变数据的函数层
// const actionCreators = { addNum, reduceNum, addNumAsync }
// App = connect(mapStateProps, actionCreators)(App);

export default App

为什么我们需要安装插件还需要配置babel才能使用装饰器呢,因为部分浏览器还未支持装饰器decorators用法。查看在线demo:Codesandbox

总结

这篇学习记录只是浅析了Redux的简单使用方式,并没有涉及到较为复杂的Redux用法,在项目开发中Redux的使用远不止这么简单,还需要进一步的摸索与实践,才能较好的掌握Redux这个强大的状态管理工具。

nice to meet you :)

参考资料:

Redux 中文文档

阮一峰Redux 入门教程(一):基本用法

阮一峰Redux 入门教程(二):中间件与异步操作

阮一峰Redux 入门教程(三):React-Redux 的用法