通常,你會透過 props 將資訊從父元件傳遞到子元件。但是,如果你必須將 props 穿過中間的許多元件,或者你的應用程式中的許多元件需要相同的資訊,那麼傳遞 props 就會變得冗長且不方便。 *Context* 讓父元件可以將某些資訊提供給它下方樹狀結構中的任何元件——無論多深——而無需透過 props 明確傳遞。
你將學到
- 什麼是「prop drilling」
- 如何使用 context 取代重複的 prop 傳遞
- context 的常見使用案例
- context 的常見替代方案
傳遞 props 的問題
傳遞 props 是一種將資料透過 UI 樹狀結構明確傳遞到使用它的元件的好方法。
但是,當你需要將某些 prop 深入傳遞到樹狀結構中,或者許多元件需要相同的 prop 時,傳遞 props 就會變得冗長且不方便。最近的共同祖先可能與需要資料的元件相距甚遠,並且將 狀態提升 到那麼高的位置可能會導致一種稱為「prop drilling」的情況。
提升狀態


Prop drilling


如果有一種方法可以將資料「傳送」到樹狀結構中需要的元件,而無需傳遞 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 發揮作用的地方。你將分三個步驟完成
- **建立**一個 context。(你可以將其稱為
LevelContext
,因為它是用於標題級別的。) - 從需要資料的元件**使用**該 context。(
Heading
將使用LevelContext
。) - 從指定資料的元件**提供**該 context。(
Section
將提供LevelContext
。)
Context 讓父元件(即使是遠距離的父元件!)可以將一些資料提供給它內部的整個樹狀結構。
在鄰近的子元件中使用 context


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


步驟 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` 來「找出」其標題級別。
- 您將 `level` prop 傳遞給 `<Section>`。
- `Section` 將其子組件包裝到 `<LevelContext.Provider value={level}>` 中。
- `Heading` 使用 `useContext(LevelContext)` 詢問上面最接近的 `LevelContext` 值。
從同一個組件使用和提供 Context
目前,您仍然必須手動指定每個區塊的 `level`。
export default function Page() {
return (
<Section level={1}>
...
<Section level={2}>
...
<Section level={3}>
...
由於 Context 讓您可以從上層元件讀取資訊,每個 Section
都可以讀取上層 Section
的 level
值,並自動將 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> ); }
現在 Heading
和 Section
都會讀取 LevelContext
來判斷它們的深度。而 Section
會將其子元件包裹在 LevelContext
中,以指定它裡面的任何東西都處於更深的層級。
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 中,不同的屬性,例如 color
和 background-color
不會互相覆蓋。您可以將所有 <div>
的 color
設定為紅色,而不會影響 background-color
。同樣地,**不同的 React Context 不會互相覆蓋。** 您使用 createContext()
建立的每個 Context 都與其他 Context 完全分離,並將使用和提供*特定* Context 的元件連結在一起。一個元件可以毫無問題地使用或提供許多不同的 Context。
在使用 Context 之前
Context 非常容易使用!然而,這也意味著它很容易被濫用。**僅僅因為您需要將某些屬性傳遞到好幾層深,並不意味著您應該將這些資訊放入 Context 中。**
在使用 Context 之前,您應該考慮以下幾種替代方案:
- **首先,**傳遞屬性**。** 如果您的元件不簡單,將十幾個屬性向下傳遞到十幾個元件並不罕見。這可能感覺很麻煩,但它可以非常清楚地表明哪些元件使用了哪些資料!維護您程式碼的人員會很慶幸您使用屬性明確地表達了資料流。
- **提取元件並將 JSX 作為
children
**傳遞給它們**。** 如果您將某些資料傳遞給許多層中間元件,而這些元件不使用該資料(僅將其進一步向下傳遞),這通常意味著您忘記了在此過程中提取一些元件。例如,也許您將資料屬性(例如posts
)傳遞給不直接使用它們的視覺元件,例如<Layout posts={posts} />
。反之,讓Layout
將children
作為屬性,並渲染<Layout><Posts posts={posts} /></Layout>
。這減少了指定資料的元件與需要資料的元件之間的層數。
如果這兩種方法都不適合您,請考慮使用 Context。
Context 的使用案例
- 佈景主題:如果您的應用程式允許使用者更改其外觀(例如深色模式),您可以在應用程式的頂部放置一個 context provider,並在需要調整視覺外觀的元件中使用該 context。
- 目前帳戶:許多元件可能需要知道目前登入的使用者。將其放入 context 中可以方便地在樹中的任何位置讀取它。某些應用程式還允許您同時操作多個帳戶(例如,以不同使用者的身分留言)。在這些情況下,將 UI 的一部分包裝到具有不同目前帳戶值的巢狀 provider 中會很方便。
- 路由:大多數路由解決方案在內部使用 context 來保存目前的路由。這就是每個連結「知道」它是否處於活動狀態的方式。如果您構建自己的路由器,您可能也想這樣做。
- 狀態管理:隨著應用程式的增長,您最終可能會在應用程式頂部附近有很多狀態。下方許多距離較遠的元件可能想要更改它。通常會將 reducer 與 context 一起使用來管理複雜的狀態並將其傳遞給遠處的元件,而不會太麻煩。
Context 不限於靜態值。如果您在下一次渲染時傳遞不同的值,React 將更新下方所有讀取它的元件!這就是 context 經常與狀態結合使用的原因。
一般來說,如果樹中不同部分的遠處元件需要某些資訊,則這很好地表明 context 將對您有所幫助。
摘要
- Context 允許元件將一些資訊提供給它下方的整個樹。
- 傳遞 context
- 使用
export const MyContext = createContext(defaultValue)
建立並匯出它。 - 將其傳遞給
useContext(MyContext)
Hook 以在任何子元件中讀取它,無論深度如何。 - 將子項包裝到
<MyContext.Provider value={...}>
中以從父項提供它。
- 使用
- Context 會穿過中間的任何元件。
- Context 讓您可以編寫「適應其周圍環境」的元件。
- 在使用 context 之前,請嘗試傳遞 props 或將 JSX 作為
children
傳遞。
挑戰 1之 1: 使用 context 取代 prop drilling
在此範例中,切換核取方塊會更改傳遞給每個 <PlaceImage>
的 imageSize
prop。核取方塊狀態保存在頂級 App
元件中,但每個 <PlaceImage>
都需要知道它。
目前,App
將 imageSize
傳遞給 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} /> ); }