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 能不用就不用,強行用非死即殘。