使用 Context 深入傳遞資料

通常,你會透過 props 將資訊從父元件傳遞到子元件。但是,如果你必須將 props 穿過中間的許多元件,或者你的應用程式中的許多元件需要相同的資訊,那麼傳遞 props 就會變得冗長且不方便。 *Context* 讓父元件可以將某些資訊提供給它下方樹狀結構中的任何元件——無論多深——而無需透過 props 明確傳遞。

你將學到

  • 什麼是「prop drilling」
  • 如何使用 context 取代重複的 prop 傳遞
  • context 的常見使用案例
  • context 的常見替代方案

傳遞 props 的問題

傳遞 props 是一種將資料透過 UI 樹狀結構明確傳遞到使用它的元件的好方法。

但是,當你需要將某些 prop 深入傳遞到樹狀結構中,或者許多元件需要相同的 prop 時,傳遞 props 就會變得冗長且不方便。最近的共同祖先可能與需要資料的元件相距甚遠,並且將 狀態提升 到那麼高的位置可能會導致一種稱為「prop drilling」的情況。

提升狀態

Diagram with a tree of three components. The parent contains a bubble representing a value highlighted in purple. The value flows down to each of the two children, both highlighted in purple.
Diagram with a tree of three components. The parent contains a bubble representing a value highlighted in purple. The value flows down to each of the two children, both highlighted in purple.

Prop drilling

Diagram with a tree of ten nodes, each node with two children or less. The root node contains a bubble representing a value highlighted in purple. The value flows down through the two children, each of which pass the value but do not contain it. The left child passes the value down to two children which are both highlighted purple. The right child of the root passes the value through to one of its two children - the right one, which is highlighted purple. That child passed the value through its single child, which passes it down to both of its two children, which are highlighted purple.
Diagram with a tree of ten nodes, each node with two children or less. The root node contains a bubble representing a value highlighted in purple. The value flows down through the two children, each of which pass the value but do not contain it. The left child passes the value down to two children which are both highlighted purple. The right child of the root passes the value through to one of its two children - the right one, which is highlighted purple. That child passed the value through its single child, which passes it down to both of its two children, which are highlighted purple.

如果有一種方法可以將資料「傳送」到樹狀結構中需要的元件,而無需傳遞 props,那不是很棒嗎?使用 React 的 context 功能,就可以做到!

Context:傳遞 props 的替代方案

Context 讓父元件可以將資料提供給它下方的整個樹狀結構。Context 有許多用途。以下是一個例子。考慮這個接受 level 作為其大小的 Heading 元件

import Heading from './Heading.js';
import Section from './Section.js';

export default function Page() {
  return (
    <Section>
      <Heading level={1}>Title</Heading>
      <Heading level={2}>Heading</Heading>
      <Heading level={3}>Sub-heading</Heading>
      <Heading level={4}>Sub-sub-heading</Heading>
      <Heading level={5}>Sub-sub-sub-heading</Heading>
      <Heading level={6}>Sub-sub-sub-sub-heading</Heading>
    </Section>
  );
}

假設你希望同一個 Section 中的標題始終具有相同的大小

import Heading from './Heading.js';
import Section from './Section.js';

export default function Page() {
  return (
    <Section>
      <Heading level={1}>Title</Heading>
      <Section>
        <Heading level={2}>Heading</Heading>
        <Heading level={2}>Heading</Heading>
        <Heading level={2}>Heading</Heading>
        <Section>
          <Heading level={3}>Sub-heading</Heading>
          <Heading level={3}>Sub-heading</Heading>
          <Heading level={3}>Sub-heading</Heading>
          <Section>
            <Heading level={4}>Sub-sub-heading</Heading>
            <Heading level={4}>Sub-sub-heading</Heading>
            <Heading level={4}>Sub-sub-heading</Heading>
          </Section>
        </Section>
      </Section>
    </Section>
  );
}

目前,你將 level prop 分別傳遞給每個 <Heading>

<Section>
<Heading level={3}>About</Heading>
<Heading level={3}>Photos</Heading>
<Heading level={3}>Videos</Heading>
</Section>

如果你可以將 level prop 傳遞給 <Section> 元件,並將其從 <Heading> 中移除,那就太好了。這樣你就可以強制同一個區塊中的所有標題都具有相同的大小

<Section level={3}>
<Heading>About</Heading>
<Heading>Photos</Heading>
<Heading>Videos</Heading>
</Section>

但是 <Heading> 元件如何知道其最接近的 <Section> 的級別?**這需要某種方式讓子元件可以「請求」樹狀結構中上方某處的資料。**

你無法單獨使用 props 來做到這一點。這就是 context 發揮作用的地方。你將分三個步驟完成

  1. **建立**一個 context。(你可以將其稱為 LevelContext,因為它是用於標題級別的。)
  2. 從需要資料的元件**使用**該 context。(Heading 將使用 LevelContext。)
  3. 從指定資料的元件**提供**該 context。(Section 將提供 LevelContext。)

Context 讓父元件(即使是遠距離的父元件!)可以將一些資料提供給它內部的整個樹狀結構。

在鄰近的子元件中使用 context

Diagram with a tree of three components. The parent contains a bubble representing a value highlighted in orange which projects down to the two children, each highlighted in orange.
Diagram with a tree of three components. The parent contains a bubble representing a value highlighted in orange which projects down to the two children, each highlighted in orange.

在遠距離的子元件中使用 context

Diagram with a tree of ten nodes, each node with two children or less. The root parent node contains a bubble representing a value highlighted in orange. The value projects down directly to four leaves and one intermediate component in the tree, which are all highlighted in orange. None of the other intermediate components are highlighted.
Diagram with a tree of ten nodes, each node with two children or less. The root parent node contains a bubble representing a value highlighted in orange. The value projects down directly to four leaves and one intermediate component in the tree, which are all highlighted in orange. None of the other intermediate components are highlighted.

步驟 1:建立 Context

首先,您需要建立 Context。您需要將其**從檔案中匯出**,以便您的組件可以使用它。

import { createContext } from 'react';

export const LevelContext = createContext(1);

`createContext` 唯一的參數是*預設*值。這裡,`1` 指的是最大的標題級別,但您可以傳遞任何種類的值(甚至是物件)。您將在下一步中看到預設值的意義。

步驟 2:使用 Context

從 React 和您的 Context 中導入 `useContext` Hook。

import { useContext } from 'react';
import { LevelContext } from './LevelContext.js';

目前,`Heading` 組件從 props 讀取 `level`。

export default function Heading({ level, children }) {
// ...
}

相反,移除 `level` prop 並從您剛導入的 Context `LevelContext` 中讀取值。

export default function Heading({ children }) {
const level = useContext(LevelContext);
// ...
}

`useContext` 是一個 Hook。就像 `useState` 和 `useReducer` 一樣,您只能在 React 組件內部立即呼叫 Hook(不能在迴圈或條件式內部)。**`useContext` 告訴 React,`Heading` 組件想要讀取 `LevelContext`。**

現在 `Heading` 組件沒有 `level` prop,您不再需要像這樣在 JSX 中將 level prop 傳遞給 `Heading`。

<Section>
<Heading level={4}>Sub-sub-heading</Heading>
<Heading level={4}>Sub-sub-heading</Heading>
<Heading level={4}>Sub-sub-heading</Heading>
</Section>

更新 JSX,使其改由 `Section` 接收它。

<Section level={4}>
<Heading>Sub-sub-heading</Heading>
<Heading>Sub-sub-heading</Heading>
<Heading>Sub-sub-heading</Heading>
</Section>

提醒一下,這是您嘗試要使其運作的標記。

import Heading from './Heading.js';
import Section from './Section.js';

export default function Page() {
  return (
    <Section level={1}>
      <Heading>Title</Heading>
      <Section level={2}>
        <Heading>Heading</Heading>
        <Heading>Heading</Heading>
        <Heading>Heading</Heading>
        <Section level={3}>
          <Heading>Sub-heading</Heading>
          <Heading>Sub-heading</Heading>
          <Heading>Sub-heading</Heading>
          <Section level={4}>
            <Heading>Sub-sub-heading</Heading>
            <Heading>Sub-sub-heading</Heading>
            <Heading>Sub-sub-heading</Heading>
          </Section>
        </Section>
      </Section>
    </Section>
  );
}

請注意,這個範例還不能完全運作!所有標題的大小都相同,因為**即使您正在*使用* Context,您也還沒有*提供*它。** React 不知道從哪裡取得它!

如果您沒有提供 Context,React 將使用您在上一步中指定的預設值。在此範例中,您將 `1` 指定為 `createContext` 的參數,因此 `useContext(LevelContext)` 傳回 `1`,將所有這些標題設定為 `<h1>`。讓我們通過讓每個 `Section` 提供自己的 Context 來解決這個問題。

步驟 3:提供 Context

`Section` 組件目前呈現其子組件。

export default function Section({ children }) {
return (
<section className="section">
{children}
</section>
);
}

**使用 Context 提供者將它們包裝起來**,以便向它們提供 `LevelContext`。

import { LevelContext } from './LevelContext.js';

export default function Section({ level, children }) {
return (
<section className="section">
<LevelContext.Provider value={level}>
{children}
</LevelContext.Provider>
</section>
);
}

這告訴 React:「如果這個 `<Section>` 內部的任何組件請求 `LevelContext`,就給它們這個 `level`。」該組件將使用其上方 UI 樹中最接近的 `<LevelContext.Provider>` 的值。

import Heading from './Heading.js';
import Section from './Section.js';

export default function Page() {
  return (
    <Section level={1}>
      <Heading>Title</Heading>
      <Section level={2}>
        <Heading>Heading</Heading>
        <Heading>Heading</Heading>
        <Heading>Heading</Heading>
        <Section level={3}>
          <Heading>Sub-heading</Heading>
          <Heading>Sub-heading</Heading>
          <Heading>Sub-heading</Heading>
          <Section level={4}>
            <Heading>Sub-sub-heading</Heading>
            <Heading>Sub-sub-heading</Heading>
            <Heading>Sub-sub-heading</Heading>
          </Section>
        </Section>
      </Section>
    </Section>
  );
}

這與原始程式碼的結果相同,但您不需要將 `level` prop 傳遞給每個 `Heading` 組件!相反,它通過詢問最接近的 `Section` 來「找出」其標題級別。

  1. 您將 `level` prop 傳遞給 `<Section>`。
  2. `Section` 將其子組件包裝到 `<LevelContext.Provider value={level}>` 中。
  3. `Heading` 使用 `useContext(LevelContext)` 詢問上面最接近的 `LevelContext` 值。

從同一個組件使用和提供 Context

目前,您仍然必須手動指定每個區塊的 `level`。

export default function Page() {
return (
<Section level={1}>
...
<Section level={2}>
...
<Section level={3}>
...

由於 Context 讓您可以從上層元件讀取資訊,每個 Section 都可以讀取上層 Sectionlevel 值,並自動將 level + 1 往下傳遞。您可以這樣做:

import { useContext } from 'react';
import { LevelContext } from './LevelContext.js';

export default function Section({ children }) {
const level = useContext(LevelContext);
return (
<section className="section">
<LevelContext.Provider value={level + 1}>
{children}
</LevelContext.Provider>
</section>
);
}

透過這個修改,您不需要將 level 屬性傳遞給 <Section><Heading>

import Heading from './Heading.js';
import Section from './Section.js';

export default function Page() {
  return (
    <Section>
      <Heading>Title</Heading>
      <Section>
        <Heading>Heading</Heading>
        <Heading>Heading</Heading>
        <Heading>Heading</Heading>
        <Section>
          <Heading>Sub-heading</Heading>
          <Heading>Sub-heading</Heading>
          <Heading>Sub-heading</Heading>
          <Section>
            <Heading>Sub-sub-heading</Heading>
            <Heading>Sub-sub-heading</Heading>
            <Heading>Sub-sub-heading</Heading>
          </Section>
        </Section>
      </Section>
    </Section>
  );
}

現在 HeadingSection 都會讀取 LevelContext 來判斷它們的深度。而 Section 會將其子元件包裹在 LevelContext 中,以指定它裡面的任何東西都處於更深的層級。

備註

這個例子使用標題層級,因為它們可以直觀地顯示巢狀元件如何覆蓋 Context。但 Context 也適用於許多其他情況。您可以向下傳遞整個子樹所需的任何資訊:目前的色彩主題、目前登入的使用者等等。

Context 會穿透中間元件

您可以在提供 Context 的元件和使用它的元件之間插入任意數量的元件。這包括內建元件,例如 <div>,以及您可能自行建構的元件。

在此範例中,相同的 Post 元件(帶有虛線框線)會在兩個不同的巢狀層級呈現。請注意,它裡面的 <Heading> 會自動從最近的 <Section> 取得其層級。

import Heading from './Heading.js';
import Section from './Section.js';

export default function ProfilePage() {
  return (
    <Section>
      <Heading>My Profile</Heading>
      <Post
        title="Hello traveller!"
        body="Read about my adventures."
      />
      <AllPosts />
    </Section>
  );
}

function AllPosts() {
  return (
    <Section>
      <Heading>Posts</Heading>
      <RecentPosts />
    </Section>
  );
}

function RecentPosts() {
  return (
    <Section>
      <Heading>Recent Posts</Heading>
      <Post
        title="Flavors of Lisbon"
        body="...those pastéis de nata!"
      />
      <Post
        title="Buenos Aires in the rhythm of tango"
        body="I loved it!"
      />
    </Section>
  );
}

function Post({ title, body }) {
  return (
    <Section isFancy={true}>
      <Heading>
        {title}
      </Heading>
      <p><i>{body}</i></p>
    </Section>
  );
}

您不需要做任何特殊的事情就能使其運作。Section 會指定其內部樹狀結構的 Context,因此您可以在任何地方插入 <Heading>,它就會具有正確的大小。請在上方的沙盒中嘗試看看!

Context 讓您可以編寫「適應其周圍環境」的元件,並根據它們被渲染的*位置*(換句話說,*在哪個 Context 中*)來改變顯示方式。

Context 的運作方式可能會讓您聯想到 CSS 屬性繼承。在 CSS 中,您可以為 <div> 指定 color: blue,而它裡面的任何 DOM 節點,無論多深,都會繼承該顏色,除非中間的其他 DOM 節點使用 color: green 覆蓋它。同樣地,在 React 中,覆蓋來自上層 Context 的唯一方法是將子元件包裹在具有不同值的 Context 提供者中。

在 CSS 中,不同的屬性,例如 colorbackground-color 不會互相覆蓋。您可以將所有 <div>color 設定為紅色,而不會影響 background-color。同樣地,**不同的 React Context 不會互相覆蓋。** 您使用 createContext() 建立的每個 Context 都與其他 Context 完全分離,並將使用和提供*特定* Context 的元件連結在一起。一個元件可以毫無問題地使用或提供許多不同的 Context。

在使用 Context 之前

Context 非常容易使用!然而,這也意味著它很容易被濫用。**僅僅因為您需要將某些屬性傳遞到好幾層深,並不意味著您應該將這些資訊放入 Context 中。**

在使用 Context 之前,您應該考慮以下幾種替代方案:

  1. **首先,**傳遞屬性**。** 如果您的元件不簡單,將十幾個屬性向下傳遞到十幾個元件並不罕見。這可能感覺很麻煩,但它可以非常清楚地表明哪些元件使用了哪些資料!維護您程式碼的人員會很慶幸您使用屬性明確地表達了資料流。
  2. **提取元件並將 JSX 作為 children**傳遞給它們**。** 如果您將某些資料傳遞給許多層中間元件,而這些元件不使用該資料(僅將其進一步向下傳遞),這通常意味著您忘記了在此過程中提取一些元件。例如,也許您將資料屬性(例如 posts)傳遞給不直接使用它們的視覺元件,例如 <Layout posts={posts} />。反之,讓 Layoutchildren 作為屬性,並渲染 <Layout><Posts posts={posts} /></Layout>。這減少了指定資料的元件與需要資料的元件之間的層數。

如果這兩種方法都不適合您,請考慮使用 Context。

Context 的使用案例

  • 佈景主題:如果您的應用程式允許使用者更改其外觀(例如深色模式),您可以在應用程式的頂部放置一個 context provider,並在需要調整視覺外觀的元件中使用該 context。
  • 目前帳戶:許多元件可能需要知道目前登入的使用者。將其放入 context 中可以方便地在樹中的任何位置讀取它。某些應用程式還允許您同時操作多個帳戶(例如,以不同使用者的身分留言)。在這些情況下,將 UI 的一部分包裝到具有不同目前帳戶值的巢狀 provider 中會很方便。
  • 路由:大多數路由解決方案在內部使用 context 來保存目前的路由。這就是每個連結「知道」它是否處於活動狀態的方式。如果您構建自己的路由器,您可能也想這樣做。
  • 狀態管理:隨著應用程式的增長,您最終可能會在應用程式頂部附近有很多狀態。下方許多距離較遠的元件可能想要更改它。通常會將 reducer 與 context 一起使用來管理複雜的狀態並將其傳遞給遠處的元件,而不會太麻煩。

Context 不限於靜態值。如果您在下一次渲染時傳遞不同的值,React 將更新下方所有讀取它的元件!這就是 context 經常與狀態結合使用的原因。

一般來說,如果樹中不同部分的遠處元件需要某些資訊,則這很好地表明 context 將對您有所幫助。

摘要

  • Context 允許元件將一些資訊提供給它下方的整個樹。
  • 傳遞 context
    1. 使用 export const MyContext = createContext(defaultValue) 建立並匯出它。
    2. 將其傳遞給 useContext(MyContext) Hook 以在任何子元件中讀取它,無論深度如何。
    3. 將子項包裝到 <MyContext.Provider value={...}> 中以從父項提供它。
  • Context 會穿過中間的任何元件。
  • Context 讓您可以編寫「適應其周圍環境」的元件。
  • 在使用 context 之前,請嘗試傳遞 props 或將 JSX 作為 children 傳遞。

挑戰 1 1:
使用 context 取代 prop drilling

在此範例中,切換核取方塊會更改傳遞給每個 <PlaceImage>imageSize prop。核取方塊狀態保存在頂級 App 元件中,但每個 <PlaceImage> 都需要知道它。

目前,AppimageSize 傳遞給 List,它將其傳遞給每個 Place,它將其傳遞給 PlaceImage。移除 imageSize prop,而是將其從 App 元件直接傳遞給 PlaceImage

您可以在 Context.js 中宣告 context。

import { useState } from 'react';
import { places } from './data.js';
import { getImageUrl } from './utils.js';

export default function App() {
  const [isLarge, setIsLarge] = useState(false);
  const imageSize = isLarge ? 150 : 100;
  return (
    <>
      <label>
        <input
          type="checkbox"
          checked={isLarge}
          onChange={e => {
            setIsLarge(e.target.checked);
          }}
        />
        Use large images
      </label>
      <hr />
      <List imageSize={imageSize} />
    </>
  )
}

function List({ imageSize }) {
  const listItems = places.map(place =>
    <li key={place.id}>
      <Place
        place={place}
        imageSize={imageSize}
      />
    </li>
  );
  return <ul>{listItems}</ul>;
}

function Place({ place, imageSize }) {
  return (
    <>
      <PlaceImage
        place={place}
        imageSize={imageSize}
      />
      <p>
        <b>{place.name}</b>
        {': ' + place.description}
      </p>
    </>
  );
}

function PlaceImage({ place, imageSize }) {
  return (
    <img
      src={getImageUrl(place)}
      alt={place.name}
      width={imageSize}
      height={imageSize}
    />
  );
}