又不是不能用系列之 Redux 快速入門

Redux 是廣泛用於 React 項目中的狀態管理工具,相似 Vue 使用的 Vuex。使用 Redux 須要記住一條相當重要的原則:能不用就不用,強行用非死即殘javascript

Redux 工做流程中的協做方

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

簡單用一下 Redux

假設有個計算器組件 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="操做數"/>&nbsp;&nbsp; <button onClick={this.handleIncrease}></button>&nbsp;&nbsp; <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 開始工做時會默認調用一次對狀態進行初始化,而第一次調用時 prevStateundefined,因此建議直接給一個初始化值避免噁心的 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="操做數"/>&nbsp;&nbsp; <button onClick={this.handleIncrease}></button>&nbsp;&nbsp; <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
複製代碼

React-Redux

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 上。返回對象的 keymapStateToProps() 裏的 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="操做數"/>&nbsp;&nbsp; <button onClick={this.handleIncrease}></button>&nbsp;&nbsp; <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="操做數"/>&nbsp;&nbsp; <button onClick={this.handleIncrease}></button>&nbsp;&nbsp; <button onClick={this.handleDecrease}></button> </div>
    )
  }
}
// 只暴露容器組件
export default connect(
  state => ({result: state}),
  {
    increase,
    decrease,
  }
)(Calculator)
複製代碼

關鍵點就一個,不要暴露「UI 組件」,只暴露「容器組件」。凡是須要和 Redux 交互數據的組件都放 src/containers 目錄下,其餘不須要和 Redux 交互的都放 src/components 目錄下。

Redux 管理多個狀態

上面的例子中 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 能不用就不用,強行用非死即殘

相關文章
相關標籤/搜索