iT邦幫忙

0

【React.js 筆記】- 使用useContext和useReducer進行多層子父元件溝通

接續自己的文章 【React.js入門 - 21】 各階層Component的溝通

在React中常常會遇到需要在多層component的之間溝通的情形。在沒有其他插件下,就必須需要經由下面這種方式一層一層傳遞。

A元素
|
|【資料】
|
v
第1層子元素
|
|【資料】
|
v
第2層子元素
.
.
.
|
v
目標子元件

即使中間的中繼元件沒有要用到資料,還是必須要幫忙綁props,這種方式在層數太多的時候就顯得很麻煩。

於是Global state的概念就出現了。

Global state

原本的A元素提供資料的對象不再只是下一層的子元素,而是A元素往下所有階層的子元素。也就是元件之間的關係只剩下

  1. 提供資料的元件(Provider) - A
  2. 使用資料的元件(Consumer) - A以下所有子元素
                |---> 第1層子元素 
                |
A元素--【資料】--|---> 第2層子元素 
                |
                |---> 第n層子元素
                
---------------------------------
 Provider       |     Consumer     

而在React常見實現Global state的方式除了使用Redux外,還有搭配React hook使用React內建的Context api。

在接下來的內容中,我們將會把以下的程式碼改成使用Context api來傳遞資料。

  • 結構
FruitStore.js
|____Amy.js
  • FruitStore.js
import React,{useState} from 'react';
import Amy from './Amy.js';

function FruitStore() {
    const [ apple, setApple ] = useState(0);
    return (
        <>
            <div className="FruitStore">目前水果店有 [ {apple} ] 個蘋果</div>
            <Amy apple={apple}/>
        </>
    );
}

export default FruitStore;
  • Amy.js
import React from 'react';

function Amy(props) {
    return (
        <div className="Amy">
            Amy看到了 [ {props.apple} ] 個蘋果
        </div>
    );
}

export default Amy;

  • 註:這裡的Amy應該要能同步顯示蘋果的數量。

Context的讀取

以下的Amy.js可以是在FruitStore子元素階層中任一層的子元素。

  1. 創造Global state的提供者(Provider)。
    透過context api實作global state的方法是接收這個api的回傳值

    React.createContext(初始值);
    

    請先建立一個FruitContext.js,並輸入以下內容:

    • FruitContext.js
    import React from "react";
    export const FruitContext = React.createContext({
        appleContext: 1,
    })
    

    在這裡,我們先創造了一個為Object的Global state,他的初始值為{appleContext:1},並用一個變數FruitContext來接收他,並透過export讓其他檔案可以引入。

  2. 在父元素引入並使用Context。
    使用Context的方法是使用<名稱.Provider value={state值}>你想要允許可以讀取這個context的子元素階層包起來。例如:

    <FruitContext.Provider value={{ appleContext:apple }} >
        <Amy/>
    </FruitContext.Provider>
    

    實作的方法如下: 先引入剛剛建立的FruitContext
    FruitStore.js

    import React,{useState} from 'react';
    import Amy from './Amy.js';
    import {FruitContext} from "./FruitContext.js";
    

    再把Amy用引入的FruitContext包起來

    import React,{useState} from 'react';
    import Amy from './Amy.js';
    import {FruitContext} from "./FruitContext.js";
    
    function FruitStore() {
        const [ apple, setApple ] = useState(0);
        return (
            <>
                <div className="FruitStore">目前水果店有 [ {apple} ] 個蘋果</div>
                <FruitContext.Provider value={{ appleContext:apple }} >
                    <Amy/>
                </FruitContext.Provider>
            </>
        );
    }
    
    export default FruitStore;
    
  3. 在需要讀取這個context的子元素中,利用useContext()這個React hook去監聽讀取的context。

    const fruitInfo=useContext(FruitContext);
    

    如上所示,此時使用fruitInfo的效果就跟一般state一樣,當FruitContext被改變時,使用到fruitInfo的這個元件就會被重新render。

    在剛剛的範例中是這樣實現的:
    Amy.js

    import React,{useContext} from 'react';
    import {FruitContext} from "./FruitContext.js";
    
    function Amy() {
        const fruitInfo=useContext(FruitContext);
        return (
        <div className="Amy">
            Amy看到了 [ {fruitInfo.appleContext} ] 個蘋果
        </div>
      );
    }
    
    export default Amy;
    

Context的修改 - 搭配useReducer實現Redux

Context也可以直接傳入函式,但我想練一下useReducer,所以這裡寫使用useReducer的方法。

useReducer是一個乍看之下有點像state和setState的工具。但運作上和setState不同的地方在,setState(傳入值)是直接把state=傳入值,但useReducer是透過預先定義好數種運算傳入參數的方式,讓操作state的呼叫者從當中擇一使用、傳入對應參數後進行運算,再將結果賦予給state。

Redux把這個「決定要如何運算傳入參數」的過程稱為reducer,呼叫者的選擇方式、傳入參數....等操作稱為action,而把呼叫者的action傳給reducer的過程稱為dispatch。

dispatch(action) -> reducer

由於筆者目前對Redux還不熟悉,建議可以查看看有關Redux更詳細、正確的流程架構。

現在,我們來用useReducer這個React hook來實現Redux。

  1. 在FruitStore.js中定義reducer(決定要如何運算傳入參數)
    如果子元素(客人)要跟FruitStore買,就把存貨減少,要賣給FruitStore,就把存貨增加。

    • 參數state: 當前的state狀況
    • 參數action: 子元素傳來的參數
    • 回傳值: 新的state的值。
    function reducer(state, action) {
        switch (action.type) {
          case 'buy':
            return state-action.value;
          case 'sell':
            return state+action.value;
          default:
            throw new Error();
        }
    }
    
  2. 引入useReducer,取得state和dispatch。
    使用useReducer的語法為

    型態 [state, dispatch] = useReducer(reducer函式, state的初始值);
    

    dispatch是有點像是setState的函式,主要功用是用它傳入action後,它會把改變前的state和action傳給reducer,讓reducer去決定要做什麼事。

    實際用在FruitStore.js中:

    import React,{useReducer} from 'react';
    const [appleState, appleDispatch] = useReducer(reducer, 3);
    
  3. 將state和dispatch放入context中,提供給子元素使用。
    要注意的是因為useReducer已經提供了state和用來設定state的dispatch,原本的state和setState就不用了。

    FruitStore.js

import React,{useReducer} from 'react';
import Amy from './Amy.js';
import {FruitContext} from "./FruitContext.js";

function FruitStore() {
    function reducer(state, action) {
        switch (action.type) {
          case 'buy':
            return state-action.value;
          case 'sell':
            return state+action.value;
          default:
            throw new Error();
        }
    }

    const [appleState, appleDispatch] = useReducer(reducer, 3);

    return (
        <>
            <div className="FruitStore">目前水果店有 [ {appleState} ] 個蘋果</div>
            <FruitContext.Provider value={{ 
                appleContext: appleState, 
                setAppleByDispatch: appleDispatch
            }} >
                <Amy/>
            </FruitContext.Provider>
        </>
    );
}

export default FruitStore;
  1. 在子元素中,使用state,並呼叫dispatch

    Amy.js

import React,{useContext} from 'react';
import {FruitContext} from "./FruitContext.js";

function Amy() {
    const fruitInfo=useContext(FruitContext);
    return (
    <div className="Amy">
        Amy看到了 [ {fruitInfo.appleContext} ] 個蘋果
        <button onClick={()=>{fruitInfo.setAppleByDispatch({type:"buy",value:1})}}>買一個蘋果</button>
    </div>
  );
}

export default Amy;

{type:"buy",value:1}就是action。

此時點擊按鍵,蘋果就會減少。如果你把action改成{type:"sell",value:1}後點擊按鍵,蘋果就會增加。

運作的流程類似是這樣(模擬而已,不等於真實的狀況)

dispatch(action={type:"buy",value:1}){
    setAppleState(reducer(appleState,action));
}

結語

最近比較沒時間,把隨手的筆記先放在這,這篇缺滿多東西的,有空的時候再回來補缺的地方QQ


尚未有邦友留言

立即登入留言