iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 28
4
Modern Web

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

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

  • 分享至 

  • xImage
  •  

(2024/04/06更新) 因應React在18後更新了許多不同的語法,更新後的教學之後將陸續放在 新的blog 中,歡迎讀者到該處閱讀,我依然會回覆這邊的提問

當專案中的階層變複雜,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
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

2 則留言

0
wrxue
iT邦好手 1 級 ‧ 2021-11-12 15:59:39

請問用了 redux 以後,經驗上來說會把所有的 state 都放到 redux 管理嗎?
有些 state 只會在單一 component 中使用,不知道這種是否也要使用 redux 管理?

Andy Chang iT邦研究生 4 級 ‧ 2021-11-13 22:35:28 檢舉

通常是會需要和其他component共用的state才會放在redux。對於純state、context、redux的使用時機通常如下:

  • Redux: 會與其他component共用的state。
  • Context: 專案不大 or 小部份的component的共用state。這是因為當context更動時,使用useContext引入該context的component都會被強制重新渲染,在專案大時應使用redux避免效能問題。所以反過來說,如果當該state變動時,所有引入該state的component都需要被更新時,就可以用context。
  • 純state: 如果是只有單一、一兩層component有用到的state,我們就不需要拉出去做狀態管理了。原因是一般在與團隊協作開發時,看到在Redux、context的state就會很直覺地認為是需要在多個地方使用到的資料,需要更動時就會特別小心去注意是否會造成錯誤side effect。沒特別需求時如果還把state拉到Redux、context,反而可能造成協作者不必要的誤會、重工。如果你只是想加入Flux架構,你可以使用useReducer

請注意這邊關於Context的敘述,由於在年初時React官方repo開啟了useContextSelector的PR,未來可能還會有變化。

總結來說這些都是大概的方向,不管是未來或是現在的專案,都可能會視遇到的情境、技術的更新,而做適當的調整。沒有絕對。

0
AndrewYEE
iT邦新手 3 級 ‧ 2023-02-06 15:34:26

目前查了一些文章,你的Flux與Redux概念講的是最清楚的,真的很感激您。
(不知道為何查詢"react Flux"中文的文章超少,且內容我都看不太懂)

我要留言

立即登入留言