prop drilling 與 context


Posted by saffran on 2021-02-25

prop drilling 介紹

有一個問題叫做 prop drilling

drill 就是「往下鑽」的意思

範例程式碼如下

Demo.js:
Demo component 裡面,會 render 出 <DemoInner />
DemoInner component 裡面,會 render 出 <DemoInnerBox />
DemoInnerBox component 裡面,會 render 出 <DemoInnerBoxContent />
DemoInnerBoxContent component 裡面,會 render 出 <button>Update title!</button> 這個按鈕

import React, { useState } from "react";

function DemoInnerBoxContent() {
  return (
    <div>
      <button>Update title!</button>
    </div>
  );
}

function DemoInnerBox() {
  return <DemoInnerBoxContent />;
}

function DemoInner() {
  return <DemoInnerBox />;
}

export default function Demo() {
  const [title, setTitle] = useState("I am title!");
  return (
    <div>
      title: {title}
      <DemoInner />
    </div>
  );
}

在 index.js 會 render 出 Demo component:

import React from "react";
import ReactDOM from "react-dom";
import Demo from "./Demo";
import reportWebVitals from "./reportWebVitals";
import { ThemeProvider } from "styled-components";

ReactDOM.render(
  <ThemeProvider theme={theme}>
    <Demo />
  </ThemeProvider>,
  document.getElementById("root")
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

UI 長這樣:

我要做的功能是:
當我按下在 DemoInnerBoxContent component 裡面的按鈕,就要去 update 「在 Demo component 裡面的 title

做法是:
當我想要從 children 去 update parent 的 state 時,就要把這個 setTitle 這個 function 從 parent (也就是 DemoInner component)傳下去

import React, { useState } from "react";

function DemoInnerBoxContent({ setTitle }) {
  return (
    <div>
      <button
        onClick={() => {
          setTitle(Math.random());
        }}
      >
        Update title!
      </button>
    </div>
  );
}

function DemoInnerBox({ setTitle }) {
  return <DemoInnerBoxContent setTitle={setTitle} />;
}

function DemoInner({ setTitle }) {
  return <DemoInnerBox setTitle={setTitle} />;
}

export default function Demo() {
  const [title, setTitle] = useState("I am title!");
  return (
    <div>
      title: {title}
      <DemoInner setTitle={setTitle} />
    </div>
  );
}

DemoInner component 傳到 DemoInnerBox component,
再從 DemoInnerBox component 傳到 DemoInnerBoxContent component 之後,
我才能在 DemoInnerBoxContent component 拿到 setTitle 這個 function

遇到的問題是:
我需要傳「很多層」之後,我才可以在 DemoInnerBoxContent component 拿到 setTitle 這個 function -> 我才能夠呼叫 setTitle 這個 function

這個問題就叫做 prop drilling:
當我的 component 有很多層的時候,我就要傳很多層才能傳到我要的地方,這樣就很麻煩,因為對於中間的那幾層(DemoInnerDemoInnerBox)只是扮演一個中介者的角色,只負責把 prop 向下傳遞而已,它們根本不需要拿到 setTitle 這個 function

而且是,每一層都要傳,不然最後面的會收不到

那有沒有辦法讓我可以從第一層直接傳到「我要的那一層」呢?

這就是 useContext 要解決的問題

useContext 簡介

React 提供了一個方法來解決 prop drilling 的問題,這個方法就是 useContext

context 就是「上下文、脈絡」的意思

useContext 的用法如下:

首先,要先引入兩個東西:useContext, createContext

import React, { useState, useContext, createContext } from "react";

接著就可以建立一個 context:const TitleContext = createContext()

createContext() 裡面,要傳入的是:TitleContext 要提供的 value 初始值

在 React 裡面,可以透過 context 把「上層的東西」傳到下層

在「我要傳遞資料的地方」,用一個 <TitleContext.Provider></TitleContext.Provider> 的 component 包住

意思就是:<TitleContext.Provider></TitleContext.Provider> 這個 component 要提供「Context 的值」

因此,在 value 就可以填入「我要傳下去的 context」,也就是 setTitle 這個 function

-> 在 <TitleContext.Provider value={setTitle}></TitleContext.Provider> 這層 component,把 context 的值設為 setTitle 並傳下去

useContext 來使用 context 傳進來的值

value 傳入什麼,在 useContext 就會回傳什麼

現在,在 TitleContext.Provider 底下的任何一層(也就是 DemoInner, DemoInnerBox, DemoInnerBoxContent),都可以使用 setTitle

DemoInnerBoxContent 裡面寫上 const setTitle = useContext(TitleContext),意思就是:我在 DemoInnerBoxContent 這層,要使用 TitleContext 這個 context 傳進來的值

透過 context 的方式,這些「中間層」就可以不用再扮演「傳遞 prop」的角色了

Demo.js:

import React, { useState, useContext, createContext } from "react"; // 先引入 useContext, createContext

const TitleContext = createContext();

function DemoInnerBoxContent() {
  const setTitle = useContext(TitleContext); // 我在 DemoInnerBoxContent 這層,要使用 TitleContext 這個 context 傳進來的值
  return (
    <div>
      <button
        onClick={() => {
          setTitle(Math.random()); // 在這裡就可以使用 setTitle 了
        }}
      >
        Update title!
      </button>
    </div>
  );
}

function DemoInnerBox() {
  return <DemoInnerBoxContent />;
}

function DemoInner() {
  return <DemoInnerBox />;
}

export default function Demo() {
  const [title, setTitle] = useState("I am title!");
  // 在 TitleContext.Provider 這層 component,把 context 的值設為 setTitle 並傳下去
  return (
    <TitleContext.Provider value={setTitle}>
      <div>
        title: {title}
        <DemoInner />
      </div>
    </TitleContext.Provider>
  );
}

value 傳下去的 context 可以是任何東西

例如:可以是一個陣列
TitleContext.Providervalue 設為 [title, setTitle] 傳下去

import React, { useState, useContext, createContext } from "react"; // 先引入 useContext, createContext

const TitleContext = createContext();

function DemoInnerBoxContent() {
  const [title, setTitle] = useContext(TitleContext); // 我在 DemoInnerBoxContent 這層,要使用 TitleContext 這個 context 傳進來的值
  return (
    <div>
      <button
        onClick={() => {
          setTitle(Math.random()); // 在這裡就可以使用 setTitle 了
        }}
      >
        Update title!
      </button>
      {title}
    </div>
  );
}

function DemoInnerBox() {
  return <DemoInnerBoxContent />;
}

function DemoInner() {
  return <DemoInnerBox />;
}

export default function Demo() {
  const [title, setTitle] = useState("I am title!");
  // 在 TitleContext.Provider 這層 component,把 context 的值設為 setTitle 並傳下去
  return (
    <TitleContext.Provider value={[title, setTitle]}>
      <div>
        title: {title}
        <DemoInner />
      </div>
    </TitleContext.Provider>
  );
}

用 context 實做出 styled component

styled component 的 <ThemeProvider></ThemeProvider>,背後就是用 Context 來實作的

index.js:

ReactDOM.render(
  <ThemeProvider theme={theme}>
    <Demo />
  </ThemeProvider>,
  document.getElementById("root")
);

自己用 context 模擬實作出 styled component

TitleContext.Provider 外面再包一層 ColorContext.Provider,所以 return 的東西就會同時有兩個 context

同樣地,在 DemoInnerBoxContent 裡面,就可以用 useContext 來使用 ColorContext

import React, { useState, useContext, createContext } from "react"; // 先引入 useContext, createContext

const TitleContext = createContext();
const ColorContext = createContext();

function DemoInnerBoxContent() {
  const [title, setTitle] = useContext(TitleContext); // 我在 DemoInnerBoxContent 這層,要使用 TitleContext 這個 context 傳進來的值
  const colors = useContext(ColorContext); // 使用 ColorContext 這個 context 傳進來的值
  return (
    <div>
      <button
        style={{
          color: colors.primary, // 在這裡就可以使用 colors 了
        }}
        onClick={() => {
          setTitle(Math.random()); // 在這裡就可以使用 setTitle 了
        }}
      >
        Update title!
      </button>
      {title}
    </div>
  );
}

function DemoInnerBox() {
  return <DemoInnerBoxContent />;
}

function DemoInner() {
  return <DemoInnerBox />;
}

export default function Demo() {
  const [title, setTitle] = useState("I am title!");
  // 在 TitleContext.Provider 這層 component,把 context 的值設為 setTitle 並傳下去
  // 在 ColorContext.Provider 這層 component,把 context 的值設為一個物件並傳下去
  return (
    <ColorContext.Provider
      value={{
        primary: "#ff0000",
      }}
    >
      <TitleContext.Provider value={[title, setTitle]}>
        <div>
          title: {title}
          <DemoInner />
        </div>
      </TitleContext.Provider>
    </ColorContext.Provider>
  );
}

動態改變傳下去的 context 值

現在,我要做的功能是:
原本按鈕的字是紅色,點擊「Click me」按鈕後,字就會變成藍色

import React, { useState, useContext, createContext } from "react"; // 先引入 useContext, createContext

const ColorContext = createContext();

function DemoInnerBoxContent() {
  const colors = useContext(ColorContext); // 使用 ColorContext 這個 context 傳進來的值
  return (
    <div>
      <button
        style={{
          color: colors.primary, // 在這裡就可以使用 colors.primary 了
        }}
        onClick={() => {
          // setTitle(Math.random()); // 在這裡就可以使用 setTitle 了
        }}
      >
        Update title!
      </button>
    </div>
  );
}

function DemoInnerBox() {
  return <DemoInnerBoxContent />;
}

function DemoInner() {
  return <DemoInnerBox />;
}

export default function Demo() {
  const [title, setTitle] = useState("I am title!");
  const [colors, setColors] = useState({
    primary: "#ff0000",
  });
  // 在 ColorContext.Provider 這層 component,把 context 的值設為一個物件並傳下去
  return (
    <ColorContext.Provider value={colors}>
      <div>
        <button
          onClick={() => {
            setColors({
              primary: "#0000ff", // 點擊按鈕,就會切換 color
            });
          }}
        >
          Click me
        </button>
        title: {title}
        <DemoInner />
      </div>
    </ColorContext.Provider>
  );
}

用這樣子的方式,可以達成「dark theme / light theme」的切換
-> 我用的顏色都是 colors.primary

  • 在 dark theme 時,colors.primary 是黑色
  • 在 light theme 時,colors.primary 是白色

context 的值,是可以在中間層被改變的

我在 Demo 這層,primary 是 "#ff0000" (紅色)
DemoInner 這層,primary 還是 "#ff0000" (紅色)
但我在 DemoInnerBox 這層,把 primary 改成 "green" (綠色)
最後,在 DemoInnerBoxContent 這層,primary 就會是 "green" (綠色)

import React, { useState, useContext, createContext } from "react"; // 先引入 useContext, createContext

const ColorContext = createContext();

function DemoInnerBoxContent() {
  const colors = useContext(ColorContext); // 使用 ColorContext 這個 context 傳進來的值
  return (
    <div>
      <button
        style={{
          color: colors.primary, // 在這裡就可以使用 colors.primary 了
        }}
        onClick={() => {
          // setTitle(Math.random()); // 在這裡就可以使用 setTitle 了
        }}
      >
        Update title!
      </button>
    </div>
  );
}

function DemoInnerBox() {
  return (
    <ColorContext.Provider
      value={{
        primary: "green",
      }}
    >
      <DemoInnerBoxContent />
    </ColorContext.Provider>
  );
}

function DemoInner() {
  return <DemoInnerBox />;
}

export default function Demo() {
  const [title, setTitle] = useState("I am title!");
  const [colors, setColors] = useState({
    primary: "#ff0000",
  });
  // 在 ColorContext.Provider 這層 component,把 context 的值設為一個物件並傳下去
  return (
    <ColorContext.Provider value={colors}>
      <div>
        <button
          onClick={() => {
            setColors({
              primary: "#0000ff", // 點擊按鈕,就會切換 color
            });
          }}
        >
          Click me
        </button>
        title: {title}
        <DemoInner />
      </div>
    </ColorContext.Provider>
  );
}

原因是:
Demo 這層,我傳入的 context 是 primary: "#ff0000" (red)
但是在下面的層,也就是 DemoInnerBox,又再傳入一個 context 是 primary: "green" (green)

因此,當我在 DemoInnerBoxContent 使用 ColorContext 時,會「往上層去找距離最近的 context」,所以就會找到 DemoInnerBox 的 context(會是 green)

styled component 的做法

那因為 <ThemeProvider> 是包在最上層,所以裡面的每一層都會拿到一樣的樣式
但其實也可以針對不同的按鈕或 component 去提供不同的 <ThemeProvider>,讓按鈕有不同的樣式

但通常,一個 App 就只會有一個「統一的 theme」

ReactDOM.render(
  <ThemeProvider theme={theme}>
    <Demo />
  </ThemeProvider>,
  document.getElementById("root")
);

useContext 實際使用情境

在一個 App 裡面,底下有很多個 component 都會需要用到同一個 state

假設是一個部落格網站的登入功能
在網站裡面的很多個 component 都會需要用到「使用者的登入狀態」這個 state,才能知道要 render 出什麼相對應的內容
例如:
有登入,才會 render 出「編輯文章的按鈕」
有登入,header 才會 render 出「管理後台的按鈕」

所以我就可以把「使用者的登入狀態」存在最上層的 state,這樣下層的 component 才可以拿到

有了 context,我就可以不用每一層 component 都傳這個 state 的值,只要在「會需要用到這個 state 的那層」用 useContext 就可以拿到 context 的值了


#React







Related Posts

# 2021 review

# 2021 review

[Golang] Reflect

[Golang] Reflect

React-[串接api篇]-註冊功能發送post ajax call

React-[串接api篇]-註冊功能發送post ajax call


Comments