Redux(mvc、flux、react-redux)

2022-09-16 12:00:15

其他章節請看:

react實戰 系列

Redux

關於狀態管理,在 Vue 中我們已經使用過 Vuex,在 spug 專案中我們使用了 mobx,接下來我們學習 Redux。

本篇以較為易懂的方式講解mvcfluxreduxreact-redux的關係、redux 的工作流以及react-redux的原理,首先通過範例講解 redux 的用法,接著用 react-redux 實現相同需求。

Tip:舊的專案倘若使用了 Redux,如果不會則無法幹活!筆者基於 spug 專案進行,其他 react 專案也一樣。

redux 簡介

MVC 和 Flux

現代應用比以前做得更多也更復雜,如果缺乏一致設計會隨著應用的增長而造成混亂,為了對抗混亂,開發者藉助 MVC 來組織應用的功能。flux(Redux)也是如此,都是為了幫助開發者處理不斷增加的複雜性。

flux 與 mvc 關注的是比應用的外觀或構件應用所用到的一些特定庫或技術更高層面的東西,關注應用如何組織、資料如何流轉,以及職責如何分配給系統的不同部分。

flux 與 mvc 不同之處在於它提倡單向資料流,並引入了一些新概念(dispatchactionstore)以及其他方面。

稍微回顧下 MVC:

  • 模型(Model) - 應用的資料,至少擁有操作關聯資料的基本方法。模型是原始資料與應用程式碼互動的地方
  • 檢視(View) - 模型的表示,通常是使用者介面。檢視中不應有與資料表示無關的邏輯。對於前端框架,通常意味著特定檢視直接與資源關聯並具有與之關聯的CURD(建立、讀取、更新、刪除)操作
  • 控制器(Controller) - 將模型和檢視邦在一起的粘合劑。只是粘合劑而不做更多的事情(不包含複雜的檢視或資料庫邏輯)

Flux 模式與 MVC 不同,沒有將應用的各部分分解為模型、檢視和控制器,而是定義了若干不同部分:

  • store - 包含應用的狀態和邏輯。有點像模型
  • action - Flux 不是直接更新狀態,而是建立修改狀態的 action 來修改應用狀態
  • view - 使用者介面。這裡指 React
  • dispatch - 對 store 進行操作的一個協調器

在Flux中,使用者操作介面,會建立一個 action,然後 dispatch 處理傳入的 action,之後將 action 傳送到 store 中更改狀態,狀態變化後通知檢視應該使用新資料。

在 mvc 中檢視和模型能彼此更新,屬於雙向資料流。而資料在 Flux 中更多的是單向流動。

flux 和 redux

flux 是一種正規化,有許多庫實現了 Flux 的核心思想,實現的方式不盡相同。

介紹一下 Flux 和 Redux 一些重要區別

  • redux 使用單一 store(即單個全域性store),將所有的東西儲存在一個地方。flux 可以有多個不同的 store
  • redux 引入 reducer,以一種更不可變的方式更改(官網:Reducer 必須是純函數)
  • redux 引入中介軟體,因為 action 和資料流是單向的,所以開發者可以在 Redux 中新增中介軟體,並在資料更新時注入自定義行為。

下圖更直觀的描述了 flux 與 redux 的區別:

  1. flux 有多個 store,redux 只有一個 store
  2. redux 有 reducer
  3. redux 也是通過 dispatch 的方式通知 store 更新狀態
  4. redux 中的 connect 用於建立容器元件,下文 react-redux 中會使用

redux 和 react-redux

redux 是狀態管理的 js 庫。

react 可以用在 vue、react、angular 中,不過更多的是用在 react。比如要在 vue 中使用狀態管理,通常會使用 vuex。

react 專案中可以直接使用 redux,但如果在配合 react-redux 就更方便一些(下文會使用到)。

redux 工作流

下圖展示了 redux 組成部分以及每個部分的職責:

  • 這裡有四部分,其中 Redux 的三個核心部分是:Action CreatorsStoreReducers
  • 大致流程:react 元件從 store 獲取狀態,通過 dispatch 傳送 action 給 store,store 通知 reducer 加工狀態,reducer 將加工後的狀態返回,然後 react 元件從 store 獲取更新後的狀態。
  • 其中 store 是單一的,而 Creators 和 Reducers 是多數。
  • Reducers 負責加工狀態初始化狀態
  • Action Creators 用於建立 action。而 action 只是一個普通 javascript 物件。

Tip:可以將上圖理解成去飯店點餐

  • Store - 老闆(一個老闆)
  • Reducers - 廚師(多個)
  • Action Creators - 服務員(多個)

客戶(react 元件)跟服務員點菜,服務員下單,老闆將選單發給廚師,廚師將食物做好,通知客戶來老闆這取餐。其中老闆是最重要的,用於聯絡服務員、廚師以及客戶;廚師是真正幹活的人。

redux

範例

我們先用兩個檔案來完成一個最簡單的 redux 範例,然後再實現一個完整版。

最簡單的版本:只使用 Store 和 Reducer。Action 由於只是普通 js 物件,暫時就不使用 Action Creators 來建立。

Tip:關於 Reducer,如果需要把 A 元件的狀態給 redux,就得給 A 構建一個 reducer,如果需要把 B 元件的狀態給 redux,就得給 B 構建一個 reducer。

需求:寫一個元件,裡面有一個數位,點選一次按鈕,數位就增加1。

// src/smallproject/MyComponent.js
import React from 'react'

export default class extends React.Component {
    state = {value: 0}
    // 預設加1
    add = (v = 1) => {
        this.setState({value: this.state.value + v})
    }

    render(){
        return <div style={{padding: 20}}>
            <p>結果值:{this.state.value}</p>
            <p><button onClick={() => this.add()}>增加</button></p>
            </div>
    }
}

Tip:在 App.js 中引入,通過 http://localhost:3000/ 即可存取:

// src/App.js
import React, { Component } from 'react';
import MyComponent from 'smallproject/MyComponent';

class App extends Component {
  render() {
    return (
      <MyComponent/>
    );
  }
}
 
 export default App;

接著把狀態交給 redux 管理,實現效果與上面範例相同,就是把 state 提取到 redux 中。請看下面步驟:

首先安裝 redux:

$ npm i redux

建立 src/redux 資料夾,用於存放 redux 的東西。

Tip:Store 用於聯絡 action、Reducer、State,位居 C 位,職責很重,不是我們用幾句程式碼就能完成。需要使用 createStore。就像這樣:

import { createStore } from 'redux'

新建兩個檔案,更新一個檔案。程式碼如下:

// src\redux\MyComponent_reducer.js
// Reducer 的職責是初始化狀態以及加工狀態
// 將初始化值提前,更清晰
const initValue = 0
export default function (state = initValue, action) {
    switch (action.type) {
      // 加工狀態
      case 'INCREMENT':
        return state + action.data
      // 初始化
      default:
        return state
    }
}
// src\redux\store.js
import { createStore } from 'redux'
import mycomponent from './MyComponent_reducer'
// 飯店開張前得把廚師準備好
export default createStore(mycomponent)
// src\smallproject\MyComponent.js

import React from 'react'

import store from '../redux/store'

export default class extends React.Component {
    componentDidMount(){
        // 監聽 store 狀態變化
        store.subscribe(() => {
            console.log(store.getState())
            // 觸發react渲染,否則我們看不到數位更新
            this.setState({})
        })
    }
    // 預設加1
    add = (v = 1) => {
        // 分發 action。這是觸發 state 變化的惟一途徑。
        // dispatch(action)
        store.dispatch({type: 'INCREMENT', data: v})
    }

    render(){
        return <div style={{padding: 20}}>
            <p>結果值:{store.getState()}</p>
            <p><button onClick={() => this.add()}>增加</button></p>
            </div>
    }
}

稍微再分析一下這三個檔案。

MyComponent_reducer 就是一個普通函數,用於初始化狀態以及加工狀態。比如增加一行偵錯語句,則可發現頁面初始化時 redux 預設傳遞了一個很奇怪的 type,可用於初始化邏輯,後續擇進入加工狀態。

const initValue = 0
export default function (state = initValue, action) {
  + console.log('state', state, 'action', JSON.stringify(action))
    switch (action.type) {
      case 'INCREMENT':
        return state + action.data
      default:
        return state
    }
}

// state 0 action {"type":"@@redux/INITv.e.f.o.y.8"} - 第一次
// state 0 action {"type":"INCREMENT","data":1}      

store.js 通過 createStore 建立 Store,並將 reducer 放入其內。

Tip:vscode 提示 import { createStore } from 'redux' 已廢棄(@deprecated We recommend using the configureStore method of the @reduxjs/toolkit package, which replaces createStore.),筆者的 redux 是 4.2.0,這裡只是理解和練習 redux,所以繼續這種方式。

MyComponent 中將 state 改為從 Store 中獲取。

  • store.getState() 獲取 redux 的狀態
  • store.dispatch() 更改 redux 的狀態
  • store.subscribe() 監聽 redux 狀態。一旦改變,需要手動觸發 react 的渲染。筆者採用 this.setState({}) 來觸發(redux 不是專門為 react 設計的),而直接呼叫 this.render() 是不會生效的。

純函數

reducer 需要純函數。比如 state 是一個陣列,如果通過 push 這類方式是不起作用的:

export default function (state = initValue, action) {
    switch (action.type) {
      case INCREMENT:
        // 不起作用。
        state.push(action)
        return state
        // 正確
        return [...state, action]
      default:
        return state
    }
}

純函數是一類特別的函數,同樣的實參比得同樣的輸出。而且還得遵守一些規則:

  • 不修改引數資料
  • 不做不靠譜的事情,比如網路請求、輸入裝置等。網路斷了...
  • 不呼叫 Date.now() 等不純的方法

優化 store.subscribe

現在一個的 store.subscribe 寫在單個元件內,如果有10個,豈不是要寫10次。我們可以將其提取到入口檔案中,就像這樣:

$ git diff src/index.js
...
+import store from './redux/store'

 moment.locale('zh-cn');
 updatePermissions();

+// 監聽 store 狀態變化
+store.subscribe(() => {
+  ReactDOM.render(
+    <Router history={history}>
+      <ConfigProvider locale={zhCN} getPopupContainer={() => document.fullscreenElement || document.body}>
+        <App/>
+      </ConfigProvider>
+    </Router>,
+    document.getElementById('root')
+  );
+})
 ReactDOM.render(
   <Router history={history}>
     <ConfigProvider locale={zhCN} getPopupContainer={() => document.fullscreenElement || document.body}>

只要 redux 狀態發生變化,就會重新執行 ReactDOM.render。前頭我們已經瞭解了 react 的 Diffing 演演算法,知道這種方式效率也不會低到哪裡去。

加入 Action Creators

首先增加兩個檔案,用於建立 action 的以及管理常數。

// src\redux\MyComponent_action.js

import {INCREMENT} from './const'

export const createIncrementAction = data => ({type: INCREMENT, data})
// src\redux\const.js

// 常數管理,防止單詞拼錯。
export const INCREMENT = 'INCREMENT'

接著修改兩個檔案,通過 git diff 顯示如下:

$ git diff
diff --git a/src/redux/MyComponent_reducer.js b/src/redux/MyComponent_reducer.js
 // src\redux\MyComponent_reducer.js
 // 將初始化值提前,更清晰
+import {INCREMENT} from './const'
 const initValue = 0
 export default function (state = initValue, action) {
     switch (action.type) {
       // 加工狀態
-      case 'INCREMENT':
+      case INCREMENT:
         return state + action.data
       // 初始化
       default:
diff --git a/src/smallproject/MyComponent.js b/src/smallproject/MyComponent.js
 import store from '../redux/store'

+import {createIncrementAction} from '../redux/MyComponent_action'
 export default class extends React.Component {

     add = (v = 1) => {
         // 分發 action。這是觸發 state 變化的惟一途徑。
         // dispatch(action)
-        store.dispatch({type: 'INCREMENT', data: v})
+        store.dispatch(createIncrementAction(v))
     }

     render(){

非同步 action

現在我們建立一個非同步增加的功能,每點選一次該按鈕,結果就會等1秒鐘在更新。

$ git diff
diff --git a/src/smallproject/MyComponent.js b/src/smallproject/MyComponent.js
@@ -16,10 +16,14 @@ export default class extends React.Component {
         store.dispatch(createIncrementAction(v))
     }

+    asyncAdd = () => {
+        setTimeout(this.add, 1000)
+    }
     render(){
         return <div style={{padding: 20}}>
             <p>結果值:{store.getState()}</p>
             <p><button onClick={() => this.add()}>增加</button></p>
+            <p><button onClick={() => this.asyncAdd()}>非同步增加</button></p>
             </div>
     }
 }

現在我決定不在元件中等待。於是我在 action 中增加一個 createIncrementAsyncAction,並在 MyComponent 中使用:

// src\redux\MyComponent_action.js

import {INCREMENT} from './const'

import store from './store'

export const createIncrementAction = data => ({type: INCREMENT, data})

// 建立一個非同步 action。不再返回物件,而是返回函數,因為在函數中我們才能做一些非同步操作
// 意思就是告訴 Store,我這裡是一個函數,需要等待 1 秒後在給我改變狀態
export const createIncrementAsyncAction = data => {
    return () => {
        setTimeout(() => {
            store.dispatch((createIncrementAction(data)))
        }, 1000)
    }
}
// src\smallproject\MyComponent.js

...
import {createIncrementAction, createIncrementAsyncAction} from '../redux/MyComponent_action'
export default class extends React.Component {
    asyncAdd = (v = 1) => {
        store.dispatch(createIncrementAsyncAction(v))
    }
    render(){
        return <div style={{padding: 20}}>
            <p>結果值:{store.getState()}</p>
            <p><button onClick={() => this.asyncAdd()}>非同步增加</button></p>
            </div>
    }
}

再次點選非同步增加,控制檯報錯如下:

// action 必須是物件...實際是函數...你可能需要中介軟體...例如 redux-thunk
redux.js:275 Uncaught Error: Actions must be plain objects. Instead, the actual type was: 'function'. You may need to add middleware to your store setup to handle dispatching other values, such as 'redux-thunk' to handle dispatching functions.

Tip: npmjs 搜尋 redux-thunk 資訊如下

// 它允許編寫具有內部邏輯的函數,這些函數可以與Redux儲存的dispatch和getState方法互動。
Thunk middleware for Redux. It allows writing functions with logic inside that can interact with a Redux store's dispatch and getState methods.

...
import { createStore, applyMiddleware } from 'redux'
import thunk from 'redux-thunk'
import rootReducer from './reducers/index'

const store = createStore(rootReducer, applyMiddleware(thunk))

...
const INCREMENT_COUNTER = 'INCREMENT_COUNTER'

function increment() {
  return {
    type: INCREMENT_COUNTER
  }
}

function incrementAsync() {
  return dispatch => {
    setTimeout(() => {
      // Yay! Can invoke sync or async actions with `dispatch`
      dispatch(increment())
    }, 1000)
  }
}

下面我們將 redux-thunk 引入進來。先安裝,然後在 store.js 中引入,最後在 action 中直接使用 redux 傳入的 dispatch 即可。

$ npm i redux-thunk
// src\redux\store.js
import { createStore, applyMiddleware } from 'redux'
import thunk from 'redux-thunk'
import mycomponent from './MyComponent_reducer'

export default createStore(mycomponent, applyMiddleware(thunk))
// src\redux\MyComponent_action.js

import {INCREMENT} from './const'

export const createIncrementAction = data => ({type: INCREMENT, data})

export const createIncrementAsyncAction = data => {
    // 這些函數可以與Redux儲存的dispatch和getState方法互動。所以就傳進來了
    return (dispatch) => {
        setTimeout(() => {
            dispatch(createIncrementAction(data))
        }, 1000)
    }
}

redux-thunk 通過包裝 store 的 dispatch 方法來工作,這樣就可以處理派發普通物件以外的東西

中介軟體的工作方式是讓開發者以一種可組合的方式介入到某個週期或流程中,意味著可以在專案中建立和使用多個相互獨立的中介軟體函數。

中介軟體(官網):

  • Middleware 最常見的使用場景是無需參照大量程式碼或依賴類似 Rx 的第三方庫實現非同步 actions。這種方式可以讓你像 dispatch 一般的 actions 那樣 dispatch 非同步 actions。
  • Middleware 可以讓你包裝 store 的 dispatch 方法來達到你想要的目的
  • redux 中介軟體從傳送 action 到 action 到達 reducer 之間的第三方擴充套件點,意味著 reduce 處理 action 之前,開發者有機會對該 action 進行操作和修改

總結:通常非同步action會呼叫同步 action,而且非同步action不是必須的,可以寫在自己元件中。

react-redux

由於 Redux 在 react 專案中用得比較多,所以出現了 react-redux(Official React bindings for Redux),方便 redux 在 react 專案中更好的使用。

使用之前我們先看一下 react-redux 的原理圖:

flowchart LR subgraph A [容器元件] UI元件 end subgraph B [Redux] Store Action_Creators Reducers end A <--通訊--> B
  • 所有UI元件外都應該包裹一個容器元件。容器元件和UI元件是父子關係
  • 容器元件用於和 Redux 互動
  • UI 元件只能通過容器元件和 redux 互動。
  • 容器元件會將 redux 的狀態、操作 redux 狀態的方法都通過 props 傳給 Ui 元件

範例

需求:將上面 redux 的範例改成 react-redux。

實現步驟如下:

  • 安裝依賴包:npm i react-redux

  • 將 MyComponent 元件改為 UI 元件,也就是去除與 Redux(或 Store)相關的程式碼即可。

// src\smallproject\MyComponent.js

import React from 'react'

import {createIncrementAction, createIncrementAsyncAction} from '../redux/MyComponent_action'
export default class extends React.Component {
   
    // 預設加1
    add = (v = 1) => {
        // 分發 action。這是觸發 state 變化的惟一途徑。
        // dispatch(action)
    }

    asyncAdd = (v = 1) => {
       
    }
    render(){
        return <div style={{padding: 20}}>
            <p>結果值:{0}</p>
            <p><button onClick={() => this.add()}>增加</button></p>
            <p><button onClick={() => this.asyncAdd()}>非同步增加</button></p>
            </div>
    }
}
  • 建立 MyComponent 對應的容器元件:
// src\container\MyComponent.js
// 這種引入方式也可以
import MyComponent from "smallproject/MyComponent";
import {connect} from 'react-redux'

// 建立容器元件。將 UI 元件放入第二個括號中,屬於固定寫法
export default connect()(MyComponent)
  • App.js 則直接使用容器元件,因為容器元件包裹了UI元件。
// src\App.js

import React, { Component } from 'react';
import MyComponent from 'container/MyComponent';
import store from './redux/store'
class App extends Component {
  render() {
    return (
      // 將 store 傳入容器元件。因為容器元件是通過 api 所建立,對方要求通過這種方式傳遞
      // 若不傳 store,控制檯會報錯:Uncaught Error: Could not find "store" in the context of "Connect(Component)".
      <MyComponent store={store} />
    );
  }
}

export default App;

Tip: 需傳入 store,否則會報錯,說沒有找到 "store"。

至此,元件應該就能正常顯示。

  • 接著我們將容器元件獲取和操作 redux 的通過 props 傳給UI元件

容器元件傳遞redux 狀態以及操作狀態的方法可以通過 connect(mapStateToProps, mapDispatchToProps) 的兩個引數,就像這樣:

$ git diff
diff --git a/src/container/MyComponent.js b/src/container/MyComponent.js

+// 獲取狀態
+const mapStateToProps = (state) => {
+    return {
+        test: 10
+    }
+}
+
+// 處理狀態
+const mapDispatchToProps = (dispatch) => {
+    return {
+        testFn: () => console.log('testFn')
+    }
+}
 // 建立容器元件。將 UI 元件放入第二個括號中,屬於固定寫法
-export default connect()(MyComponent)
+export default connect(mapStateToProps, mapDispatchToProps)(MyComponent)

diff --git a/src/smallproject/MyComponent.js b/src/smallproject/MyComponent.js
export default class extends React.Component {
     add = (v = 1) => {
+        this.props.testFn()
     }
     render(){
         return <div style={{padding: 20}}>
-            <p>結果值:{0}</p>
+            <p>結果值:{this.props.test}</p>
             </div>

Tip:mapStateToProps 傳遞的是狀態,所以是 key:value 的物件;mapDispatchToProps 傳遞的是操作狀態的方法,所以是 key: valueFn;如果傳遞一個不正確的格式,就像這樣 connect(() => {}, () => {})(MyComponent),控制檯會報錯如下

mapStateToProps() in Connect(Component) must return a plain object. Instead received undefined.

mapDispatchToProps() in Connect(Component) must return a plain object. Instead received undefined.

實現

上面範例我們分析了實現需求的核心步驟,最終程式碼如下:

  • 容器元件包裹UI元件,並給UI組價提供與 Redux 互動的狀態和操作狀態的方法:
// src\container\MyComponent.js
// 這種引入方式也可以
import MyComponent from "smallproject/MyComponent";
import {connect} from 'react-redux'
import {createIncrementAction, createIncrementAsyncAction} from 'redux/MyComponent_action'
// 獲取狀態
const mapStateToProps = (state) => {
    return {
        value: state
    }
}

// 處理狀態
const mapDispatchToProps = (dispatch) => {
    return {
        increment: (v) => dispatch(createIncrementAction(v)),
        asyncIncrement: (v) => dispatch(createIncrementAsyncAction(v))
    }
}
// 建立容器元件。將 UI 元件放入第二個括號中,屬於固定寫法
export default connect(mapStateToProps, mapDispatchToProps)(MyComponent)

Tip:mapDispatchToProps 還有一種簡寫方式,傳物件。react-redux 會自動幫我們 dispatch。

// 簡寫
// 上面的寫法都有 dispatch,都是將引數傳給 createAction
const mapDispatchToProps =  {
    increment: createIncrementAction,
    asyncIncrement: createIncrementAsyncAction
}
  • UI 元件通過父元件(容器元件)與Redux互動:
// src\smallproject\MyComponent.js

import React from 'react'
export default class extends React.Component {
    // 預設加1
    add = (v = 1) => {
        // 分發 action。這是觸發 state 變化的惟一途徑。
        // dispatch(action)
        this.props.increment(v)
    }

    asyncAdd = (v = 1) => {
        this.props.asyncIncrement(v)
    }
    render(){
        return <div style={{padding: 20}}>
            <p>結果值:{this.props.value}</p>
            <p><button onClick={() => this.add()}>增加</button></p>
            <p><button onClick={() => this.asyncAdd()}>非同步增加</button></p>
            </div>
    }
}

無需 store.subscribe

前面我們學習 redux 時,redux 狀態更新後需要手動觸發 react 渲染,於是寫了下面這段程式碼:

// 監聽 store 狀態變化
store.subscribe(() => {
  ReactDOM.render(
    <Router history={history}>
      <ConfigProvider locale={zhCN} getPopupContainer={() => document.fullscreenElement || document.body}>
        <App/>
      </ConfigProvider>
    </Router>,
    document.getElementById('root')
  );
})

現在用 react-redux ,即使刪除這段程式碼,上面範例也能正常執行。因為容器元件幫我們做了這件事。

Provider 提供 store

前面我們寫了一個容器元件,並給容器元件傳遞了一次 store(<MyComponent store={store}/>),如果有100個容器元件,豈不是要傳遞100次。

react-redux 中的 Provider 只要我們寫一次,它會自動幫我們把 store 傳給裡面的所有容器元件(容器元件是通過 connent 建立,也就能夠被識別出來)。我們將其引入:

$ git diff
diff --git a/src/App.js b/src/App.js
  import React, { Component } from 'react';
  import MyComponent from 'container/MyComponent';
- import store from './redux/store'
  class App extends Component {
    render() {
      return (
-      // 將 store 傳入
-      <MyComponent store={store}/>
+      <MyComponent/>
      );
    }
  }
 
diff --git a/src/index.js b/src/index.js
+import { Provider } from 'react-redux';

 ReactDOM.render(
   <Router history={history}>
     <ConfigProvider locale={zhCN} getPopupContainer={() => document.fullscreenElement || document.body}>
-      <App/>
+      <Provider store={store}>
+        <App />
+      </Provider>
+
     </ConfigProvider>
   </Router>,
   document.getElementById('root')

合併UI元件和容器元件

上面例子,我們將一個元件分成UI元件和容器元件,也就是1個變2個,如果100個元件,豈不是變成200個元件!檔案數量急劇上升。

我們可以將 UI 元件放入容器元件中來解決此問題,因為對外只需要暴露容器元件。就像這樣:

// src\container\MyComponent.js
import React from 'react'
import {connect} from 'react-redux'
import {createIncrementAction, createIncrementAsyncAction} from 'redux/MyComponent_action'

// UI 元件
class MyComponent extends React.Component {
    ...
}
...

export default connect(mapStateToProps, mapDispatchToProps)(MyComponent)

兩個元件資料相互共用

上面範例只在一個元件中進行,實際工作至少得2個元件才有使用 redux(資料共用)的必要。

需求:為了更真實的使用 redux,這裡我們再新建一個元件(Count),讓兩個元件相互讀取對方的值。

目前 redux 資料夾下,一個元件就有一個 action 一個 reducer,在建立 Count 元件,則又得增加兩個檔案,而且每個檔案後都寫了一遍 action 或 reducer,有些繁瑣。

Administrator@3L-WK-10 MINGW64 /e/lirufen/spug/src/redux
$ ll
total 4
-rw-r--r-- 1 Administrator 197121 335 Sep  1 15:45 MyComponent_action.js
-rw-r--r-- 1 Administrator 197121 362 Sep  1 14:29 MyComponent_reducer.js
-rw-r--r-- 1 Administrator 197121 102 Sep  1 14:33 const.js
-rw-r--r-- 1 Administrator 197121 224 Sep  1 15:44 store.js

我們首先優化一下目錄結構。在 redux 目錄中新建兩個資料夾,分別用來儲存 action 和 reducer。就像這樣:

// 新建兩個資料夾
$ mkdir actions reducers
// 移動檔案到 actions,並重新命名為 MyComponent.js
$ mv MyComponent_action.js ./actions/MyComponent.js
$ mv MyComponent_reducer.js ./reducers/MyComponent.js

然後複製 MyComponent 的action、reducer以及容器元件,重新命名為 Count 元件即可。

現在有一個問題:建立Store時要傳入 reducers,當前我們傳遞的只是一個元件的 reducer,現在我們有連個元件,對應兩個 reducer 該如何傳遞?

// src\redux\store.js
import { createStore, applyMiddleware } from 'redux'
import thunk from 'redux-thunk'
import mycomponent from './reducers/MyComponent'
export default createStore(mycomponent, applyMiddleware(thunk))

Tip: 現在傳的第一個引數就是 mycompoment,而且從 redux 中取值也是 state

// 直接取 state
const mapStateToProps = (state) => {
    return {
        value: state
    }
}

傳遞多個 reducers 得使用 combineReducers 組合 reduces。最終 store.js 程式碼如下:

// src\redux\store.js
import { createStore, applyMiddleware, combineReducers } from 'redux'
import thunk from 'redux-thunk'
import mycomponent from './reducers/MyComponent'
import count from './reducers/Count'

// 傳給combineReducers的reduxData就是存在 redux 中的資料,所以取值得換成 state.value1
const reduxData = {value1: mycomponent, value2: count}

const allReducers = combineReducers(reduxData)
export default createStore(allReducers, applyMiddleware(thunk))

最終調整一下元件雙方的取值,就實現元件資料的共用了。

// MyComponent 元件取得的 Count 的值
const mapStateToProps = (state) => {
    return {
        value: state.value2
    }
}
// Count 元件取得的 MyComponent 的值
const mapStateToProps = (state) => {
    return {
        value: state.value1
    }
}

:有了 react-redux 你還需要 redux。在 npmjs 的 react-redux:You'll also need to install Redux and set up a Redux store in your app.,而且你從現在程式碼也能發現,react-redux 主要用於建立容器元件,而建立 store 還得通過 redux 完成。

最終程式碼

可執行的完整程式碼如下:

Tip:只保留了 MyComponent 元件,因為 Count 也類似。

index.js

增加了 Provider。

// src\index.js
...
import { Provider } from 'react-redux';

moment.locale('zh-cn');
updatePermissions();

ReactDOM.render(
  <Router history={history}>
    <ConfigProvider locale={zhCN} getPopupContainer={() => document.fullscreenElement || document.body}>
      <Provider store={store}>
        <App />
      </Provider>
    </ConfigProvider>
  </Router>,
  document.getElementById('root')
);

serviceWorker.unregister();

App.js

// src\App.js
import React, { Component, Fragment } from 'react';
import MyComponent from 'container/MyComponent';
class App extends Component {
  render() {
    return (
      <Fragment>
        {/* <Count /> */}
        <MyComponent />
      </Fragment>
    );
  }
}

export default App;

store.js

// src\redux\store.js
import { createStore, applyMiddleware, combineReducers } from 'redux'
import thunk from 'redux-thunk'
import mycomponent from './reducers/MyComponent'

// 傳給combineReducers的reduxData就是存在 redux 中的資料,所以取值得換成 state.value1
const reduxData = {value1: mycomponent}

const allReducers = combineReducers(reduxData)
export default createStore(allReducers, applyMiddleware(thunk))

const.js

// src\redux\const.js

// 常數管理,防止單詞拼錯。
export const INCREMENT = 'INCREMENT'
export const INCREMENT2 = 'INCREMENT2'

reducers\MyComponent.js

// src\redux\reducers\MyComponent.js
// 將初始化值提前,更清晰
import {INCREMENT} from 'redux/const'
const initValue = 0
export default function (state = initValue, action) {
    switch (action.type) {
      // 加工狀態
      case INCREMENT:
        return state + action.data
      // 初始化
      default:
        return state
    }
}

actions\MyComponent.js

// src\redux\actions\MyComponent.js

import {INCREMENT} from 'redux/const'

export const createIncrementAction = data => ({type: INCREMENT, data})

export const createIncrementAsyncAction = data => {
    return (dispatch) => {
        setTimeout(() => {
            dispatch(createIncrementAction(data))
        }, 1000)
    }
}

container\MyComponent.js

// src\container\MyComponent.js
// 這種引入方式也可以
import {connect} from 'react-redux'
import {createIncrementAction, createIncrementAsyncAction} from 'redux/actions/MyComponent'
import React from 'react'

class MyComponent extends React.Component {
    // 預設加1
    add = (v = 1) => {
        // 分發 action。這是觸發 state 變化的惟一途徑。
        // dispatch(action)
        this.props.increment(v)
    }

    asyncAdd = (v = 1) => {
        this.props.asyncIncrement(v)
    }
    render(){
        return <div style={{padding: 20}}>
            <p>結果值:{this.props.value}</p>
            <p><button onClick={() => this.add()}>增加</button></p>
            <p><button onClick={() => this.asyncAdd()}>非同步增加</button></p>
            </div>
    }
}
// 獲取狀態
const mapStateToProps = (state) => {
    return {
        value: state.value1
    }
}

// 簡寫
// 上面的寫法都有 dispatch,都是將引數傳給 createAction
const mapDispatchToProps =  {
    increment: createIncrementAction,
    asyncIncrement: createIncrementAsyncAction
}

// 建立容器元件。將 UI 元件放入第二個括號中,屬於固定寫法
export default connect(mapStateToProps, mapDispatchToProps)(MyComponent)

package.json

{
  "name": "spug",
  "version": "3.0.0",
  "private": true,
  "dependencies": {
    ...
    "react-redux": "^8.0.2",
    "redux": "^4.2.0",
    "redux-thunk": "^2.4.1",

  },

其他章節請看:

react實戰 系列