Redux 是广泛用于 React 项目中的状态管理工具,相似 Vue 使用的 Vuex。使用 Redux 须要记住一条相当重要的原则:能不用就不用,强行用非死即残。javascript
React Components 就是开发者本身编写的组件,须要改变状态时建立一个 Action 分发给 Store,须要获取状态时直接从 Store 拿。java
Action Creators 负责建立 Action,实际就是写一堆函数来建立不一样的 Action,每一个 Action 都是只有两个属性的平平无奇的对象。属性 type
表示这个 Action 要干啥,值永远是字符串;属性 data
表示作 type
表示的这件事须要的数据,值能够是任何类型。node
{
type: string
data: any
}
复制代码
Store 是整个 Redux 的核心。当收到一个 Action 时负责将其维护的状态的当前值和 Action 打包发给 Reducer 进行状态变动,Reducer 变动结束后保存变动结果。react
Reducers 是个没有感情的打工人,根据 Store 发过来的 Action 对状态进行对应变动并返回给 Store。shell
假设有个计算器组件 Calculator
,它能够加减输入的操做数,最后把计算结果显示出来。在不使用 Redux 的状况下实现以下:redux
src/components/Calculator/index.jsx
后端
import React, { Component } from 'react'
export default class Calculator extends Component {
state = {
result: 0
}
handleIncrease = () => {
const {result} = this.state
const value = this.node.value
this.setState({
result: result + (value * 1)
})
}
handleDecrease = () => {
const {result} = this.state
const value = this.node.value
this.setState({
result: result - (value * 1)
})
}
render() {
return (
<div> <h2>当前计算结果:{this.state.result}</h2> <input ref={c => this.node = c} type="text" placeholder="操做数"/> <button onClick={this.handleIncrease}>加</button> <button onClick={this.handleDecrease}>减</button> </div>
)
}
}
复制代码
开始 Redux 改造前记得安装一下 Redux,Redux 相关的文件一般放置在 src/redux
目录下,这样便于管理。数组
yarn add redux
复制代码
上文提到 Action 的 type
属性永远是字符串,而且 Reducer 也要接收这个 Action,那么用脚想一下也知道 type
会处处用。字符串满天飞最可怕的事情是什么?那确定是同一个字符串在不一样的使用位置出现拼写错误。因此一般会在 src/redux
目录下建立一份 JS 文件来专门维护 Action 的 type
。在上面的示例中咱们要作的 Action 有两个,一个是「加」,一个是「减」。markdown
src/redux/constants.js
网络
export const CalculatorActionType = {
INCREASE: 'increase',
DECREASE: 'decrease',
}
复制代码
一般全局只须要一份常量文件,因此最好按照不一样组件建立对象进行维护,避免出现不一样组件之间重名的状况,也避免维护的时候看着满屏的常量懵逼。常量名和对应的值随便写,你本身看得懂又不会被合做的同事打死就行。
接下来是建立 Action Creator 来生成不一样的 Action,实际上就是在一个 JS 文件里面写一堆函数生成 Action 对象。
src/redux/actions/calculator.js
import {CalculatorActionType} from '../constants' // 引入常量
export const increase = data => ({type: CalculatorActionType.INCREASE, data})
export const decrease = data => ({type: CalculatorActionType.DECREASE, data})
复制代码
返回值是一个对象,用小括号包裹避免把大括号识别为函数体,还用了对象简写等 JS 的骚操做(做为后端表示记住就行,JS 骚操做太多学不完)。
有了 Action 接下来就是召唤打工人 Reducer 进行状态变动。Reducer 按照要求必定要是一个「纯函数」,简单理解就是输入参数不容许在函数体中被改变,返回的必定是个新的东西,具体概念自行 wiki。
src/redux/reducers/calculator.js
import {CalculatorActionType} from '../constants'
const initState = 0 // 初始化值
export default function calculatorReducer(prevState = initState, action) {
const {type, data} = action
switch (type) {
case CalculatorActionType.INCREASE:
return prevState + data
case CalculatorActionType.DECREASE:
return prevState - data
default:
return prevState
}
}
复制代码
函数名能够随便改无所谓,第一个参数是要变动状态的当前值,即此时此刻 Store 里面保存的状态值,第二个参数就是上面建立的 Action 对象。函数内部就是 switch 不一样的 ActionType 作不一样的状态变动。须要注意的是,不要在 Reducer 里面去作网络请求和数据计算,这些工做放到业务组件或者 Action Creator 里完成。不要问为何,记住就行!Reducer 在 Redux 开始工做时会默认调用一次对状态进行初始化,而第一次调用时 prevState
是 undefined
,因此建议直接给一个初始化值避免恶心的 undefined
。
最后就是建立 Redux 中的资本家 Store。建立 Store 以前必定要有 Reducer,不然资本家没有能够 996 压榨的打工人。Store 全局只须要一份。
src/redux/store.js
import {createStore} from 'redux'
import calculatorReducer from './reducers/calculator'
export default createStore(calculatorReducer)
复制代码
createStore()
返回一个 Store 对象,因此必需要暴露出去,不然无法在组件里用。到这里 Redux 就能用了,接下来就是去组件里使用。将原来基于组件 state
的示例改造以下:
src/components/Calculator/index.jsx
import React, { Component } from 'react'
import store from '../../redux/store' // 引入 store
import {increase, decrease} from '../../redux/actions/calculator' // 引入 action
export default class Calculator extends Component {
// 订阅 Redux 维护的状态的变化
componentDidMount() {
store.subscribe(() => {
this.setState({})
})
}
handleIncrease = () => {
const value = this.node.value
store.dispatch(increase(value * 1)) // 使用 store 分发 action
}
handleDecrease = () => {
const value = this.node.value
store.dispatch(decrease(value * 1)) // 使用 store 分发 action
}
render() {
return (
<div> {/* 从 store 上获取维护的状态的值 */} <h2>当前计算结果:{store.getState()}</h2> <input ref={c => this.node = c} type="text" placeholder="操做数"/> <button onClick={this.handleIncrease}>加</button> <button onClick={this.handleDecrease}>减</button> </div>
)
}
}
复制代码
对操做数的加减运算经过 Store 实例调用 dispatch()
函数分发 Action 来完成,Action 的建立也是调用对应的函数,value * 1
是为了把字符串转换成数字。 展现计算结果时调用 getState()
函数从 Redux 获取状态值。
在组件挂载钩子 componentDidMount
中调用 Store 的 subscribe()
函数进行 Store 状态变化的订阅,当 Store 维护的状态发生变化时会触发监听,而后执行一次无心义的组件状态变动,目的是为了触发 render()
函数进行页面从新渲染,不然 Store 中维护的状态值没法更新到页面上。
补充一点,Action 分为同步和异步两种。若是返回的是一个对象就表示同步 Action,若是返回的是一个函数就表示异步 Action。
export const increaseAsyn = (data, delay) => {
return (dispatch) => {
setTimeout(() => {
dispatch(increase(data))
}, delay)
}
}
复制代码
这里用一个定时器来模拟异步任务,实际上这里能够是发起网络请求获取数据。异步 Action 内部必定是调用同步 Action 来触发状态变动。异步 Action 须要单独的库支持:
yarn add redux-thunk
复制代码
而后须要对 Store 配置进行修改:
import {createStore, applyMiddleware} from 'redux' // 引入 applyMiddleware
import thunk from 'redux-thunk' // 引入 thunk
import calculatorReducer from './reducers/calculator'
export default createStore(calculatorReducer, applyMiddleware(thunk)) // 应用 thunk
复制代码
Redux 和 React 其实没有一毛钱关系,只是恰好名字长得像而已。可是 Redux 在 React 项目中使用的很是多,因此 React 官方就基于 Redux 作了层封装,目的是让 Redux 更好用,然而我并不以为好用了多少。
React-Redux 搞出了「容器组件」和「UI 组件」两个概念。「容器组件」是「UI 组件」的父组件,负责与 Redux 打交道,也就是分发 Action 通知 Store 改变状态值和从 Redux 获取状态值。这样「UI 组件」就彻底和 Redux 解耦,本来对 Redux 的直接操做经过「容器组件」代理完成。「容器组件」与「UI 组件」经过 props
进行交互。使用前先安装库:
yarn add react-redux
复制代码
改造流程就两步,建立一个「容器组件」,而后把 Calculator 改形成符合规范的「UI 组件」放在「容器组件」中。
src/containers/Calculator/index.jsx
import {connect} from 'react-redux'
import Calculator from '../../components/Calculator/index'
export default connect()(Calculator)
复制代码
容器组件一般放在 src/containers
目录下,和「UI 组件」的 src/components
目录进行区分,固然这不是必须的,你开心就好。「容器组件」的建立就是使用 connect()
函数链接「UI 组件」,不要问为何,记住就行!有了「容器组件」后就不用在 App.js
中使用「UI 组件了」,而应该换成「容器组件」。
import React, { Component } from 'react'
import store from './redux/store' // 引入 store
import Calculator from './containers/Calculator' // 引入容器组件
export default class App extends Component {
render() {
return (
<div> <Calculator store={store}/> </div>
)
}
}
复制代码
记得引入 Store 并经过 props
传递给 Calculator 容器组件,不然无法作状态操做。若是有不少个「容器组件」都须要传递 Store,能够直接在入口文件 index.js
中使用 <Provider>
组件一把梭,这样就不用在 App.js
里写了。
import React from 'react';
import ReactDOM from 'react-dom';
import store from './redux/store' // 引入 store
import {Provider} from 'react-redux' // 引入 Provider 组件
import App from './App';
ReactDOM.render(
<Provider store={store}> <App /> </Provider>,
document.getElementById('root')
);
复制代码
使用 <Provider>
组件还有一个好处是不用使用 store.subscribe()
订阅 Redux 状态变动了,<Provider>
已经提供了这个功能。
回到容器组件 Calculator 中,connect()
函数能够接收两个函数用于将 Redux 维护的状态和变动状态的行为映射到「UI 组件」的 props
上。
import {connect} from 'react-redux'
import Calculator from '../../components/Calculator/index'
import {increase, decrease} from '../../redux/actions/calculator'
function mapStateToProps(state) {
return {result: state}
}
function mapDispatchToProps(dispatch) {
return {
increase: data => dispatch(increase(data)),
decrease: data => dispatch(decrease(data)),
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Calculator)
复制代码
mapStateToProps()
将 Redux 维护的状态值映射到「UI 组件」(不是「容器组件」)的 props
上,你别管怎么映射,照着规范写就好了。返回值是一个对象,key
表示映射到 props
后的 key
。也就是你在「UI 组件」里调用 this.props.xxx
时那个 xxx
的名字。
mapDispatchToProps()
将 Redux 操做状态的动做映射到「UI 组件」(不是「容器组件」)的 props
上。返回对象的 key
和 mapStateToProps()
里的 key
一个意思。
固然这样写有点麻烦,因此能够来个骚操做简写一波:
import {connect} from 'react-redux'
import Calculator from '../../components/Calculator/index'
import {increase, decrease} from '../../redux/actions/calculator'
export default connect(
state => ({result: state}),
{
increase,
decrease,
}
)(Calculator)
复制代码
这里骚得有点厉害,稍微解释一下。首先是把 mapStateToProps()
和 mapDispatchToProps()
两个函数直接放到参数位置,不在外部定义。而后 React-Redux 支持 mapDispatchToProps()
里直接写 Action,而不用多余写 dispatch()
:
export default connect(
state => ({result: state}),
{
increase: increase,
decrease: decrease,
}
)(Calculator)
复制代码
而后两个建立 Action 的函数的名字恰好和 key
一致,因此使用对象简写语法变成了极简版。到这里就把 Redux 维护的状态值和变动状态的动做都映射到「UI 组件」的 props
上了,接下来改造「UI 组件」。
src/components/Calculator/index.jsx
import React, { Component } from 'react'
export default class Calculator extends Component {
handleIncrease = () => {
const value = this.node.value
this.props.increase(value * 1) // 调用 props 上的方法
}
handleDecrease = () => {
const value = this.node.value
this.props.decrease(value * 1) // 调用 props 上的方法
}
render() {
return (
<div> {/* 从 props 上取值 */} <h2>当前计算结果:{this.props.result}</h2> <input ref={c => this.node = c} type="text" placeholder="操做数"/> <button onClick={this.handleIncrease}>加</button> <button onClick={this.handleDecrease}>减</button> </div>
)
}
}
复制代码
如今就不须要在「UI 组件」上引入 Store 和 Action 了,直接从 props
上调用方法和取值便可。
原来写一个组件只须要一个文件,引入 React-Redux 后如今须要写两个文件,一份「UI 组件」,一份「容器组件」,多少有些麻烦,并且组件多了之后容易把眼睛看瞎。因此能够把「UI 组件」和「容器组件」写到一份文件里。
src/containers/Calculator/index.jsx
import React, { Component } from 'react'
import {connect} from 'react-redux'
import {increase, decrease} from '../../redux/actions/calculator'
// UI 组件不暴露
class Calculator extends Component {
handleIncrease = () => {
const value = this.node.value
this.props.increase(value * 1)
}
handleDecrease = () => {
const value = this.node.value
this.props.decrease(value * 1)
}
render() {
return (
<div> <h2>当前计算结果:{this.props.result}</h2> <input ref={c => this.node = c} type="text" placeholder="操做数"/> <button onClick={this.handleIncrease}>加</button> <button onClick={this.handleDecrease}>减</button> </div>
)
}
}
// 只暴露容器组件
export default connect(
state => ({result: state}),
{
increase,
decrease,
}
)(Calculator)
复制代码
关键点就一个,不要暴露「UI 组件」,只暴露「容器组件」。凡是须要和 Redux 交互数据的组件都放 src/containers
目录下,其余不须要和 Redux 交互的都放 src/components
目录下。
上面的例子中 Redux 都只管理了一个数字类型的状态,实际开发中 Redux 确定要管理各类类型五花八门的状态。好比现有另一个书架组件 BookShelf,它须要委托 Redux 管理全部的书籍。按照套路,先在定义全部操做书籍的 ActionType。
src/redux/constants.js
export const CalculatorActionType = {
INCREASE: 'increase',
DECREASE: 'decrease',
}
export const BookShelfActionType = {
ADD: 'add' // 添加图书
}
复制代码
而后建立 Action Creator 生成 Action:
import {BookShelfActionType} from '../constants'
export const add = data => ({type: BookShelfActionType.ADD, data})
复制代码
而后写 Reducer 的状态变动逻辑:
import {BookShelfActionType} from '../constants'
const initState = [
{id: '001', title: 'React 从入门到入土', auther: '子都'}
]
export default function bookshelfReducer(prevState = initState, action) {
const {type, data} = action
switch (type) {
case BookShelfActionType.ADD:
return [data, ...prevState] // 这里用了展开运算符
default:
return prevState
}
}
复制代码
上文已经说过,store.js
全局只会有一份,这时候出现了两个 Reducer 就须要作一些处理。
import {createStore, applyMiddleware, combineReducers} from 'redux'
import thunk from 'redux-thunk'
import calculatorReducer from './reducers/calculator'
import bookshelfReducer from './reducers/bookshelf'
const combinedReducers = combineReducers(
{
result: calculatorReducer,
books: bookshelfReducer,
}
)
export default createStore(combinedReducers, applyMiddleware(thunk))
复制代码
使用 combineReducers()
函数将多份 Reducer 合并为一个,使用合并后的 Reducer 建立 Store。这里要注意 combineReducers()
函数中的对象参数,这里的概念绕不清那基本就废了。
首先要明确,每一个 Reducer 只能管理一个状态,这里的「一个」能够是一个数字、数组、对象、字符串等等,反正只能是「一个」。那么 combineReducers()
中,key
就是你给状态取的名字,而 value
就是管理这个状态的 Reducer。这里的命名会影响你在「UI 组件」经过 props
获取状态值时的 key
。
多个 Reducer 合并后,Calculator 「容器组件」的取值就不能直接取 state,而是取 state 上的 result:
export default connect(
state => ({result: state.result}), // 从 state 上取 result
{
increase,
decrease,
}
)(Calculator)
复制代码
最后看一下新增的 BookShelf 组件的结构,仅简单作了数据展现:
src/containers/BookShelf/index.jsx
import React, { Component } from 'react'
import { connect } from 'react-redux'
import {add} from '../../redux/actions/bookshelf'
class BookShelf extends Component {
render() {
return (
<div> <ul> { this.props.books.map((book) => { return <li key={book.id}>{book.name} --- {book.auther}</li> }) } </ul> </div>
)
}
}
export default connect(
state => ({books: state.books}), // 从 state 的 books 上取值,对应 store 中合并 Reducer 时的 key
{
add
}
)(BookShelf)
复制代码
记住本文开篇的那句话,Redux 能不用就不用,强行用非死即残。