iT邦幫忙

2023 iThome 鐵人賽

DAY 14
0
Modern Web

react 學習記錄系列 第 14

[Day14]我的 react 學習記錄 - createContext & useContext

  • 分享至 

  • xImage
  •  

這篇文章的主要內容

簡單介紹 createContext 跟 useContext 的用途跟使用方法。


createContext & useContext

createContext 跟 useContext 最常被用來解決的問題就是 props drilling,在開發過程中通常會希望把元件切分的越小越好,以方便共用跟維護,當元件越切越多層的時候,為了共享 state,就會把 state 透過 props 的方式一層層向下傳遞,反而影響了 code 的可讀性。

context-1

像上面這樣單純的功能,寫成 code 可能會像下面這樣。

import { useState } from "react";

type Props = {
  isLight: boolean;
};

function Card({ isLight }: Props) {
  return (
    <div style={{ backgroundColor: isLight ? "#ccc" : "#333" }}>
      <p style={{ color: isLight ? "#333" : "#ccc" }}>My Card</p>
    </div>
  );
}

function Board({ isLight }: Props) {
  return (
    <div style={{ margin: "10px" }}>
      <p>Board</p>
      <Card isLight={isLight} />
    </div>
  );
}

function App() {
  const [isLight, setIsLight] = useState<boolean>(false);

  function toggleLight() {
    setIsLight(!isLight);
  }

  return (
    <>
      <div style={{ display: "flex" }}>
        <Board isLight={isLight} />
      </div>
      <button onClick={toggleLight}>Toggle Light</button>
    </>
  );
}

在 App 創建一個 state 然後用 toggleLight 來控制 Card 裡面元素的顏色變化,然後一層層地往下傳讓最裡面的 Card 可以取得這個 state,中間經過的 <Board> 甚至沒有使用到這個變數,只有傳遞 props 的作用,如果專案變大,會越來越難以閱讀跟追蹤 state 的位置。
這個時候可以透過 createContext 跟 useContext 的組合來解決這個 props drilling 的情形。


createContext

createContext 就像他的命名一樣,用來建立一個 context 可以讓多個元件存取一個 context。

Syntax

const SomeContext = createContext(defaultValue)

defaultValue: 預設 value,當使用 useContext 或是 <SomeContext.Consumer> 的元件並不是 <SomeContext.Provider> 的子元件時就會拿到這個資料,避免出現錯誤。

SomeContext: 必須要大寫開頭,這是一個 context 物件,裡面有兩個我們需要知道的屬性。

  1. SomeContext.Provider 讓我們把資料提供到子元件。
  2. SomeContext.Consumer 讓我們可以取得 SomeContext.Provider 所提供的資料。

上面的範例如果改用 createContext 改寫會變成這樣

import { useState, createContext } from "react";

const LightContext = createContext(false);

function Card() {
  return (
    <LightContext.Consumer>
      {(value) => (
        <div style={{ backgroundColor: value ? "#ccc" : "#333" }}>
          <p style={{ color: value ? "#333" : "#ccc" }}>My Card</p>
        </div>
      )}
    </LightContext.Consumer>
  );
}

function Board() {
  return (
    <div style={{ margin: "10px" }}>
      <p>Board</p>
      <Card />
    </div>
  );
}

function App() {
  const [isLight, setIsLight] = useState<boolean>(false);

  function toggleLight() {
    setIsLight(!isLight);
  }

  return (
    <>
      <div style={{ display: "flex" }}>
        <LightContext.Provider value={isLight}>
          <Board />
        </LightContext.Provider>
      </div>
      <button onClick={toggleLight}>Toggle Light</button>
    </>
  );
}

把你希望獲得資料的元件用 <LightContext.Provider> 包起來,這個 Provider 接收一個 value 屬性,value 就放著你希望共享的資料,這邊我放的是 isLight

然後在子元件可以透過 <LightContext.Consumer> 的元件來取得 value,<LightContext.Consumer> 裡面必須要放入一個 render function,而這個 render function 裡面就可以得到 Provider 所提供的 value,但是因為可讀性,現在已經 不建議 用這個方式來取得 value 了,現在都用 useContext 來取得 value。

透過 Context 一樣可以在子元件取得想要的 props 來做到相同的效果,還可以提升 code 的可讀性跟維護性。

context-2


useContext

用來取得 <LightContext.Provider> 所提供的 value。

Syntax

const value = useContext(SomeContext)

SomeContext: 用 createContext 建立的 context
value: <SomeContext.Provider> 所提供的 value

注意事項

  • value 改變時 react 會 re-render 被 <SomeContext.Provider> 包起來的所有元件。
  • useContext 只能在元件裡使用,且只能在元件的最外層使用,不能放在迴圈或是判斷式裡。

上面的範例用 useContext 改寫會變成這樣,因為 useContext 其實只是 <SomeContext.Consumer> 的語法糖而已。

function Card() {
  const value = useContext(LightContext);
  return (
    <div style={{ backgroundColor: value ? "#ccc" : "#333" }}>
      <p style={{ color: value ? "#333" : "#ccc" }}>My Card</p>
    </div>
  );
}

使用 useContext 改寫之後整個可讀性有很明顯的上升,不需要使用 <SomeContext.Consumer> 把特定的區域包起來,然後在裡面寫 render function。


useContext 永遠都會找最近的 Provider 拿資料

context 還有另外一個用途,就是相同的元件可以包裹不同的 Provider 來共享不同的資料或是參照不同的資料。
假設在 main.tsx 的檔案例我建立了一個 SizeContext 物件,並且把 value 傳到整個 <App/> 裡面。

export const SizeContext = createContext(12);
ReactDOM.createRoot(document.getElementById("root")!).render(
  <React.StrictMode>
    <SizeContext.Provider value={12}>
      <App />
    </SizeContext.Provider>
  </React.StrictMode>
);

然後在 App.tsx 裡面在不同的元件裡使用 useContext 來取得 value 並且建立不同的 Provider,給其他子元件使用。

import { useContext } from "react";
import { SizeContext } from "./main";

function Card() {
  const value = useContext(SizeContext);
  return (
    <div style={{ border: "solid 1px #000" }}>
      <p style={{ fontSize: `${value}px` }}>Card</p>
    </div>
  );
}

function Board() {
  const value = useContext(SizeContext);
  return (
    <div style={{ border: "solid 1px #000" }}>
      <p style={{ fontSize: `${value}px` }}>Board</p>
      <SizeContext.Provider value={36}>
        <Card />
      </SizeContext.Provider>
      <Card /> // 這一個元件沒有被 Provider 包起來
    </div>
  );
}

function App() {
  const value = useContext(SizeContext);

  return (
    <>
      <div style={{ border: "solid 1px #000" }}>
        <p style={{ fontSize: `${value}px` }}>App</p>
        <SizeContext.Provider value={24}>
          <Board />
        </SizeContext.Provider>
      </div>
    </>
  );
}

畫面會長這樣。

https://ithelp.ithome.com.tw/upload/images/20230919/20161583YDXXP1DjP1.png

兩個相同的 <Card> 元件一個有被 <Board> 裡面的 Provider 包起來,所以取得到的 context 會不同,導致顯示在畫面上的 font-size 會不同。


分開 Context

如果資料是不會變動的東西時,通常會盡量把資料分開,然後再使用 useContext 取得資料時,可以透過放入不同的 context 來獲取不同的資料,提高可讀性跟維護性。

const userInfo = { name: "Evan", age: "20" };

export const SizeContext = createContext(12);
export const UserContext = createContext(userInfo);

ReactDOM.createRoot(document.getElementById("root")!).render(
  <React.StrictMode>
    <UserContext.Provider value={userInfo}>
      <SizeContext.Provider value={12}>
        <App />
      </SizeContext.Provider>
    </UserContext.Provider>
  </React.StrictMode>
);

createContext - react document
useContext - react document
Passing Data Deeply with Context - react document

下一篇簡單介紹useReducer。
如果內容有誤再麻煩大家指教,我會盡快修改。

這個系列的文章會同步更新在我個人的 Medium,歡迎大家來看看 👋👋👋
Medium


上一篇
[Day13]我的 react 學習記錄 - react forwardRef & useImperativeHandle
下一篇
[Day15]我的 react 學習記錄 - useReducer
系列文
react 學習記錄30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言