iT邦幫忙

第 12 屆 iT 邦幫忙鐵人賽

DAY 28
1
Modern Web

從比入門再往前一點開始,一直到深入React.js系列 第 28

【Day.28】React進階 - 導入Redux,讓元件溝通更簡潔

當專案中的階層變複雜,state和props變的很多,資料在多層component之間的傳遞也越來越多。產生了一堆純粹用來傳遞用的props和父component。

工程師心想: 「有沒有一個全局的state和setState可以讓所有的元件共同操作呢?」

於是Global State的概念就誕生了。

Global State的概念就像是住宅大廈的公共設施,它不單獨屬於任何一個人,也能夠被任何人取用。

【Day.17】React入門 - 利用useContext進行多層component溝通中,我們提及了React本身提供的狀態管理API。但是我們也在後續的文章中講到了Context API本身並不是設計拿來做大型專案的狀態管理,因而存在麻煩的效能問題。

現在,就讓我們來認識最被廣為使用的狀態管理套件 - Redux。

Redux 的由來

Redux在2015年誕生。他不只是一個普通的全局state和setState工具而已,Redux受到了Facebook提出的設計概念Flux啟發。有關Flux誕生的原因,我們在【Day.27】React進階 - 用useReducer定義state的更動原則 有說明。

總之,我們不應該讓別人能夠隨意修改state,而是要預先定義好修改的規則,並讓其他開發者透過這些規則來操作。

同時,我們也說過在Flux觀念下,我們操作state的過程大概變成像這樣:

  1. 管理者預先定義好有哪些規則(action)可以使用
  2. 管理者預先定義好規則(action)對應到的邏輯運算(store)是什麼。
  3. 操作者透過一個溝通用的函式(dispatch),把他選擇的規則(action)和需要的參數(payload)丟給管理者
  4. 流程/資料透過管理者規定好的方式更新

上圖截自Facebook對於flux的說明影片

Redux的運作流程

Redux基於上述Flux的架構外,又做了一些補充和修改

  1. 管理者預先定義好有哪些state可以使用,並採用Single source,讓所有人拿到的state是一樣、共用的。
  2. 管理者預先定義好有哪些修改規則(action)可以使用
  3. 管理者預先定義好規則(action)對應到的邏輯運算(reducer)是什麼。
  4. Redux把所有state和對應的reducer包成一起,稱為store
  5. 透過一個Provider把store提供給專案中所有的元件

而操作者可以有兩種操作選擇:

  1. 操作者可以透過一個selector,從store裡面取出想要的state
  2. 操作者可以透過一個溝通用的函式(dispatch),把他選擇的規則(action)和需要的參數(payload)丟給管理者

依據上述流程,我們就能在任何地方取得state,同時state也會透過管理者規定好的方式更新。


上圖引用自Redux官方文件

Redux使用

1. 安裝

首先,請先打開terminal,輸入

npm install --save redux react-redux 

Redux和專為React打造的react-redux就會被安裝。

2. 設定action和定義reducer

請在src底下新增model資料夾,並在裡面建立reducer.js

useReducer一樣,當操作者呼叫dispatch後,Redux會呼叫Reducer函式。Reducer函式的語法是:

  • 接收兩個參數
    • 第一個是state之前的值
    • 第二個則是操作者傳入dispatch函式的參數。
  • Reducer必須要有一個回傳值,該值會變成state新的值。

請注意第一次執行的時候,reducer的第一個參數(state)如果有給default value,該default value就會變成state的初始值。

const initState = {
    menuItemData: [
        "Like的發問",
        "Like的回答",
        "Like的文章",
        "Like的留言"
    ],
  };

const itemReducer = (state = initState, action) => {
    switch (action.type) {
      case 'ADD_ITEM': {
        const menuItemCopy = state.menuItemData.slice();
        return { menuItemData: [action.payload.itemNew].concat(menuItemCopy) };
      }
      case 'CLEAN_ITEM': {
        return { menuItemData: [] };
      }
      default:
        return state;
    }
};

export {itemReducer};

而雖然我們這裡是直接把action字串定義在reducer中,但比較好的方式應該是讓action字串也用變數來管理,並用該變數來判斷action:

const ADD_ITEM = 'ADD_ITEM';
const CLEAN_ITEM = 'CLEAN_ITEM';

3. 用store包覆action和reducer

在src/model底下新增store.js

要創立store的話,必須要使用redux提供的APIcreateStore

import {createStore} from "redux";

接著引入剛剛定義好的reducer,丟給createStore就能產生store了

  • src/model/store.js
import {createStore} from "redux";
import {itemReducer} from "./reducer.js";

const itemStore = createStore(itemReducer); 

export {itemStore};

createStore還可以吃一些middleware參數,幫redux多加一些功能,下一篇我們會提。

如果你有多個reducer要包起來,可以使用combineReducers這個API

import {createStore} from "redux";
import {itemReducer, otherReducer} from "./reducer.js";

const store = createStore(combineReducers(
    itemReducer,
    otherReducer 
)); 

export {itemStore};

4. 使用Provider包覆所有元件

Provider是react-redux提供的特殊React元件,被<Provider></Provider>包住的元件都能恣意取用store裡面的state。它的語法是

<Provider store={store}> 

</Provider>

現在,我們回到所有React程式的起點,引入Provider和剛剛建立的itemStore,用它包住所有程式。

  • src/index.js
import { Provider } from "react-redux";
import { store } from "./model/store.js";

ReactDOM.render(   
    <Provider store={store}> 
        <App/>
    </Provider>,
    document.getElementById('root')
);

我個人會習慣把Provider放在App內的最外層,但這裡我覺得這樣示意對新手比較好理解。

接著就能在裡面的元件取用state了。但在function component使用Redux的方式會比較特別,必須要使用Redux提供的hook

使用useSelector取得state

React-Redux提供了一個hookuseSelector,能讓我們在React function component中選取想要從Redux取得的state。

import { useSelector } from 'react-redux';

useSelector本身需要一個參數,此參數為函式,定義了你要如何從所有state中挑選你需要的state。

例如,由於剛剛我們定義的state結構為:

{
    menuItemData: [
        "Like的發問",
        "Like的回答",
        "Like的文章",
        "Like的留言"
    ],
};

useSelector會把所有的state丟入我們定義的函式參數中,我們取得menuItemData的方式就是從參數函式把它單獨取出並回傳:

const menuItemData = useSelector(state => state.menuItemData);

menuItemData變數就會是我們需要的state。

使用useDispatch取得dispatch

React-Redux提供的另一個hookuseDispatch,能讓我們在React function component中呼叫dispatch函式。

import { useDispatch } from 'react-redux';

使用上很簡單很單純,先把這個函式取出來:

const dispatch = useDispatch();

然後想要更動state時直接呼叫它就可以。呼叫dispatch時記得要傳「想要選擇的更動規則、想要傳的參數」。 詳細你可以回頭去看剛剛的reducer是怎麼定義的

  • 新增item
dispatch({
    type: "ADD_ITEM",
    payload: {itemNew:"測試資料"}
}); 
  • 清空item
dispatch({ type: "CLEAN_ITEM" }); 

加入Redux到我們的程式碼吧

store、reducer那些的實作跟上面的範例完全一樣,我就不再寫一次了。這裡我只有實作把先前的menuItemData搬到Redux後,如何在MenuPage引入的作法,你可以自己試試看如何把isOpen搬進Redux。

  • src/page/MenuPage.js
import React, { useReducer,useMemo,useEffect} from 'react';

import MenuItem from '../component/MenuItem';
import Menu from '../component/Menu';
import { OpenContext } from '../context/ControlContext';

import { useSelector, useDispatch } from 'react-redux';

const reducer = function(state, action){
    switch(action.type){
        case "SWITCH":
            return !state;
        default:
            throw new Error("Unknown action");
    }
}

const MenuPage = () =>{
    const [isOpen, isOpenDispatch] = useReducer(reducer,true);

    const menuItemData = useSelector(state => state.menuItemData);
    const dispatch = useDispatch();

    let menuItemArr = useMemo(()=>menuItemData.map((wording) => <MenuItem text={wording} key={wording}/>),[menuItemData]);

    return (
        <OpenContext.Provider value={{ 
            openContext: isOpen, 
            setOpenContext: isOpenDispatch
        }} >
            <Menu title={"Andy Chang的like"}>
                {menuItemArr}
            </Menu>
            <button onClick={()=>{
                dispatch({
                    type: "ADD_ITEM",
                    payload: {itemNew:"測試資料"}
                }); 
            }}>更改第一個menuItem</button>
        </OpenContext.Provider>
    );
}

export default MenuPage;

Redux家族

因為Redux本身還不夠用,近年來又衍伸出了各式各樣的Redux版本和middleware,例如:

  • Redux-Actions

    把redux的流程封裝簡化

  • Redux-Saga

    著重於redux的非同步處理

  • Redux-Thunk

    把redux的非同步處理再更簡化

  • Redux-Observable

    以functional-programming的方式處理資料流

下一篇我們就會來聊如何使用Redux-Thunk。


上一篇
【Day.27】React進階 - 用useReducer定義state的更動原則
下一篇
【Day.29】React進階 - 以Redux Thunk處理非同步資料流
系列文
從比入門再往前一點開始,一直到深入React.js30

尚未有邦友留言

立即登入留言