[React] useContext & useReducer 基本使用方法筆記

Basic React Hook

useContext 與 useReducer 為 React 原生的 Hook,本次的範例將會搭配使用做出類似 Redux 有中央儲存庫以及 state 管理系統的功能。

KNOW WHAT

useContext

在沒有使用 useContext 或狀態管理工具的時候,通常如果父元素要傳遞 props 到子元素或甚至是"孫元素",都必須要層層的傳遞下去,但使用了 useContext 後就能創造類似"中央儲存庫"的系統(像是 Redux 中的 store),當子元素需要用到父元素的 state 值時,能夠透過 Context Api 內的方法直接取得,不需要在 components 之間傳來傳去,但最好確保有多個 components 都需要用到某 state 的值才使用 Context API (e.g.深淺色主題、登入狀態 )。

useReducer

當專案中有較為複雜的 state 需要管理的時候,可以透過 useReducer 來取代 useState,使用 useReducer 的好處是能夠同時管理多個 state 值,並且得透過 action 來修改 state 值,避免取用上混亂,當同時有非常多的 state 需要使用時,會是非常好的工具 。


KNOW HOW

這次要使用的範例是在 React 生態系中出現一萬遍的超好用範例  - Counter 數計器 將會同時使用 Context 來於組件中傳遞 porps (state value),並且使用 useReducer 來管理 state,雖然需要管理的 state 只有一個,但此範例將會運用這兩個 React 原生的 hook 來做出像是 Redux 的功能。


範例程式架構與簡介

程式架構

CodeSandbox
GitHub

在這個迷你程式中會顯示一個 counter value,與 “+” 跟 “-” 的按鈕,按 “+” 的話 counter value 就 +1,按 “-” 則 -1,如果點擊 counter value 值將會變回 0。


建立 Reducer

首先必須先建立一個 Reducer,何謂 Reducer?

Reducer 有點像是一個媒介,讓你可以透過傳進來的 action 對 state 值進行對應動作, action 顧名思義就是"動作",並且是你自己可以定義的(在某些情況下必須攜帶額外的資訊時 action 內還會有 payload),Reducer 會帶進一個 state 與 action 作為參數。

在 App.js 內創建並使用 Reducer,必須先初始化我們的 state:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10

const initialState = {
  count: 0
};

const App = () => {
{......}
}

export default App;

創建 Redcuer:

這裡會帶進兩個參數, state 與 action, state 會透過 reducer 所接收的 action 來決定如何改變它的值,action 則是從其他地方分派(dispacth)過來的,會帶著各種資訊,像是 action.type 就是告訴 reducer 現在要執行的 action type。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

const reducer = (state, action) => {
  switch (action.type) {
    case "increment":
      return {
        count: state.count + 1
      };
    case "decrement":
      return {
        count: state.count - 1
      };
    case "reset":
      return {
        count: (state.count = 0)
      };
    default:
      return {
        count: state
      };
  }
};

const initialState = {
  count: 0
};

const App = () => {
{......}
}

export default App;

通常會用 switch 語句來判斷從其他地方分派過來的 action.type 是什麼,並且做對應的處理。

呼叫 useReducer hook 並把我們剛所建立的 reducer 跟 initialState 傳入:

1
2
3
4
5
6
7

const App = () => {
  const conuterReducer = useReducer(reducer, initialState);
{......}
}

export default App;

因為要使用待會要使用 useContext 把此 Reducer 傳到各個 components 中,所以定義了 counterReducer,如果單獨只使用 useReducer:

1
const [state, dispatch] = useReducer(reducer, initialState);

如此一來就可以直接讀取 state 的值,並透過 dispatch 的 action 來改動 state 的值。

成功建立好 useReducer 啦~


建立 Context

建立 context 資料夾,並且建立 MyContext.js 檔案:

1
2
3
import { createContext } from "react";

export const MyContext = createContext();

createContext(defaultValue) 內可以指定預設值,但我們這裡不使用。

再來回到 App.js 要引入定義好的 MyContext,並將他傳到子元素中: MyContext 引入之後會變成像 Component,必須用 MyContext 把需要傳遞值的 Child Component 子元素給包裹起來,並且加入 Context Provider 與 value 屬性

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import React, { useReducer } from "react";

import "./styles.css";
import Counter from "./components/Counter";
import { MyContext } from "./context/MyContext";

const reducer = (state, action) => {
{......}
};

const initialState = {
  count: 0
};

const App = () => {
  const conuterReducer = useReducer(reducer, initialState);

  return (
    <React.Fragment>
      <MyContext.Provider value={conuterReducer}>
        <Counter />
      </MyContext.Provider>
    </React.Fragment>
  );
};

export default App;

然後在 value 中傳入剛剛定義好的 useReducer - counterReducer,這麼一來在 MyContext 內的所有子元素都可以存取 state 與 dispatch 方法啦~


使用 Context  中的數據

在這個 Counter 程式中會有兩個 Component 需要用到 Context 中的值,也就是在 components 資料夾內的 Counter 與 ValueDispaly,照理來說可以在 Counter 內一次完成,但我刻意多包了一層,多展現一點 Context 的方便之處

先到我們的 Counter 組件內,並且引入 React 的 useContext hook 與我們剛剛獨立創建的 MyContext:

1
2
import React, { useContext } from "react";
import { MyContext } from "../context/MyContext";

接著使用 useContext hook,並且傳入參數告訴它要使用那一個 Context (也就是我們一起 import 的 MyContext):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
{......}
import styles from "./Counter.module.scss";
import ValueDisplay from "./ValueDisplay";

const Counter = () => {
  const [state, dispatch] = useContext(MyContext);

  return (
    {......}
  );
};

export default Counter;

這樣就拿到了剛才在 App.js 內當作 value 傳過來的 state 與 dispatch 了

Counter.js 所返回的 JSX 將會 render Counter 的元素,並且帶 ValueDisplay 組件  -  顯示與重置 Counter 的值:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
const Counter = () => {
  const [state, dispatch] = useContext(MyContext);

  return (
    <React.Fragment>
      <div className={styles.counterContainer}>
        <h2>Counter</h2>
        <ValueDisplay />
        <div className={styles.btnField}>
          <button onClick={() => dispatch({ type: "increment" })}>+</button>
          <button onClick={() => dispatch({ type: "decrement" })}>-</button>
        </div>
      </div>
    </React.Fragment>
  );
};

export default Counter;

在 “+” 跟 “-” 按鈕中,帶入 onClick 事件, 一旦此按鈕被按了之後,就會執行 dispatch 分派 action。

dispatch 方法可以分派 action ,帶入 { type:“action name” } 來指定 action,並送交給 reducer 來對 state 做對應的處理。

這裡要特別注意的是 dispatch 是一個有帶參數的 function,所以我們在 onClick={……} 之中要使用箭頭函數,再放入 dispatch 與它的參數,不可以直接把帶參數的 dispatch({……}) 放在 onClick 事件內(適用在其他狀況)。

在 ValueDisplay 內也是一樣的: 必須先引入 useContext hook 與 MyContext,並且告訴 useContext hook,請給我們 MyContext

1
2
3
4
5
6
7
8
9
import React, { useContext } from "react";
import { MyContext } from "../context/MyContext";

const ValueDisplay = () => {
  const [state, dispatch] = useContext(MyContext);
  return <p onClick={() => dispatch({ type: "reset" })}>{state.count}</p>;
};

export default ValueDisplay;

這裡的 action type 為 reset,因此點擊了以後 reducer 會把 state 歸零


完成程式:

大功告成啦~

不過 useContext 其實使用上還是沒有像 Redux 或 Redux Tool Kit 那麼靈活,像是如果單獨使用 useReducer 是沒有辦法存取中央儲存庫內的 state,必須搭配 useContext。

而在使用 useContex 上要注意,一但裡面的 state 有改變,有使用到的 child components 也會一併 rerender,所以在設計的時候要多思考一下。

以上為我所理解的概念,如果有錯誤還請大神指正,希望可以一起創造更強大的社群 👍


Reference

React useReducer Hook

React useContext Hook

【react】context VS redux

終究都要學 React 為何不現在學呢? - React 進階  - useReducer - (15)

React Hook(二): Context 和 useContext

使用 Hugo 建立
主題 StackJimmy 設計