【 React 】Context 入門

Jamie Lo
12 min readMar 10, 2023

--

Photo by Johannes Plenio on Unsplash

本篇筆記摘錄自官方文件 Passing Data Deeply with Context

在先前的文章內,我們有提到如何使用 props 的方式,將資訊由父元件傳送至子元件,如果需要接收資訊的子元件與父元件之間相隔多個元件,又或者是有多個子元件需要接收同一個資訊,原本的方式將會讓程式碼變得冗長且難以管理。

透過使用 Context,我們可以將資訊傳至任何子元件。

The problem with passing prop

傳遞 props 是一個透過 UI tree 傳遞資訊到其他子元件的好方法。

但當 props 傳遞的目標地點為樹狀結構深處,或是多個元件需要同一個 prop,就會使程式碼變得冗長,最近距離的共同父元件可能離需要資訊的子元件非常遠,從高處 lifting state up 的情形被稱為 “prop drilling”。

有沒有辦法可以不透過這樣的方式來傳遞資料呢?
有的!context 就是為了解決這個問題而誕生的。

Context: an alternative to passing props

Context 讓父元件可以提供資料給該樹狀結構層底下的所有子元件。

下方的這個範例裡,Heading元件透過接收level來決定字體大小:

( 將左側拉桿向右移動,可以看見程式碼的部分,拉開後點擊左上角三條線漢堡圖示可以看見完整檔案,右邊則為輸出結果。 )

假設你希望同一個section裡的 headings 大小一致:

目前我們是將levelprop 分別傳入<Heading>

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

levelprop移至<Section>元件會是一個比較好的做法,此方法可以確保相同的 section 裡,headings 擁有相同的大小:

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

但是<Heading>要如何取得最接近<Section>裡的level呢?我們必須要有一些方法,讓子元件可以向該樹狀結構以上的某個地方 “請求” 資料。

單靠 props 是沒有辦法完成的,我們可以透過下面三個步驟來使用 context:

  1. 創建 context
    ( 由於是用來存放 heading level,我們可以命名為LevelContext)
  2. 在需要資料的元件使用 context
    (Heading將會使用LevelContext)
  3. 擁有指定資料的元件提供 ( provide ) context
    (由Section提供LevelContext)

Context 讓父元件可以提供資料給該樹狀結構以下的所有子元件。

步驟一:創建 context

首先,創建 context,你需要將它從檔案匯出,這樣其他元件才能夠使用:

createContext的唯一引數為預設值,這邊的1指的是最大的 heading level,你可以傳入任何類型的值 ( 物件也可以 )。

步驟二:使用 context

匯入useContext 以及你的 context:

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

目前Heading元件是從 props 取得 level

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

請移除levelprop,改為使用剛剛匯入的LevelContext

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

useContext是一個 Hook,就跟useStateuseReducer一樣,你只能在元件的最頂層呼叫它們,useContext 將會告訴 React,Heading元件想要讀取LevelContext

現在Heading元件不再使用levelprop,我們不再需要像下方這樣在 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>

改由Section來接收 level:

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

提醒,目前我們嘗試運作的標記式語言長這樣:

透過結果可以發現,目前尚未達成我們想要的樣貌,目前所有 heading 的大小都是一樣的,這是因為雖然我們已經使用了 context,但我們尚未提供 ( provide ) context,React 並不知道要從哪裡取得資訊。

如果沒有提供 context,React 將會使用我們在步驟一所設置的預設值,在這個範例裡,我們將createContext的引數設置為1,所以useContext(LevelContext)會回傳1,這會使得所有的 heading 都被設為<h1> ,我們可以透過提供 context 給Section來解決這個問題。

步驟三:Provide the context

目前Section元件渲染著它的 children:

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

請使用 context provider 將它們包覆起來,將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 tree 結構以上最接近的<LevelContext.Provider>所提供的值。
( 如果到這邊覺得不是很好理解,可以繼續往下看範例,透過實際範例將會好理解許多。)

輸出結果與一開始的範例是相同的,但我們不再需要向每個Heading傳遞levelprop,它會透過請求父層中最接近的Section來 “計算” 出 heading 大小:

  1. 傳遞levelprop 到 <Section>
  2. Section將它的 children 包進<LevelContext.Provider value={level}>
  3. Heading透過useContext(LevelContext)請求最靠近的LevelContext

Using and providing context from the same component

目前我們是透過手動指定 section 的level

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

由於 context 讓我們可以從父層取得資料,且每一個Section都能從父層Section取得level,因此我們可以改為傳遞level + 1,讓它自動的傳遞下去:

( 預設值改為 0,這樣一層一層包下去就會變成 h1 ~ h6,因為 context 會找最接近的父層。 )

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>

範例選用 heading level 是因為在視覺上能夠看出明顯的差異,context 可以用在許多地方,你可以使用它來傳遞資訊到所有 subtree,像是主題顏色,或是目前登入的使用者是誰 … 等。

( subtree 指的是該樹狀結構以下的內容,以 root 來說,所有的內容都是它的 subtree。)

Context passes through intermediate components

你可以在 provide context 的元件間放入任何需要的元件,包含像是<div>這樣的內建元件,以及你自己所建立的元件。

以下方的範例來說,同樣的Post( 虛線外框 ) 渲染在兩個不同的巢狀 level,請注意裡面的<Heading>,它的level是自動透過最接近的<Section>獲得:

你不需要特別去做什麼,Section會指定樹狀結構內的 context,因此你可以在裡面的任何地方放進<Heading>,並且得到正確的尺寸。

Context 會讓你的元件去 “適應它的環境”,並且根據渲染位置有著不同的依據 ( 指的是在哪個 context 裡 )。

個人覺得官方文件以上這兩段非常饒舌,簡單來說,使用 context 包覆元件,而內部元件會因為所處不同層級,而受到不同的 context 值影響,因為元件它會參照最接近它自己的父層。

context 的運作就像 CSS 的繼承。
例如在<div>指定color: blue,所有在這個<div>內的 DOM 節點,無論層級有多深,都會繼承color: blue,除非你在中間覆寫color: green
同樣地,在 React 裡,唯一覆寫 context 的方式就是將 children 包覆在不同值的 context provider 內。

在 CSS中,不同的屬性 ( 像是colorbackground-color) 不會覆蓋彼此,同樣地,不同的 React contexts 也不會覆寫彼此,每一個你用createContext()創造出來的 context 都是彼此分開的,同一個元件使用或 provider 多個不同的 context 是完全沒有問題的。

Before you use context

context 是一個誘人使用的東西,這意味這它容易被過度使用。
需要傳遞 props 到一些較深層級處,不代表就必須使用 context。

這裡有一些你在使用 context 前可以考慮的選擇:

  1. 從傳遞 props 開始,如果元件並不瑣碎,將多個 props 傳入多個元件並不是不行的,這個方法能讓元件的資料來源清楚明瞭,利於維護。
  2. 提取元件並使用來children傳遞內容,如果有一筆資料通過多層中間元件,而那些中間的元件並不需要這份資料 ( 只是將資料傳到更遠處 ),這通常意味著你忘記提取元件。
    舉例來說,相較於將postsprop 傳遞給不會直接使用到的元件
    <Layout posts={posts} />
    不如把它直接放在用得到它的地方
    <Layout><Posts posts={posts} /></Layout>
    這樣的方法可以減少元件的層級,並直接把資料指定給需要的接收者。

如果以上這兩種方法都不適合,再考慮使用 context。

Use cases for context

以下提供一些適合使用 context 的情境:

  • Theming:如果你的 app 可以讓使用者變更它的外觀 ( 像是 dark mode ),你可以在 app 中的最頂層放上 context provider,並且在那些需要轉換外觀的元件上使用 context。
  • Current account:若有多個元件需要知道現在登入的使用者是誰,將它放進 context 可以讓我們更方便於在各個地方讀取資訊,有些 app 會讓你同時操作不同帳號 ( 像是使用不同的身份留下評論 ),在那種情況下,將部分 UI 包覆進不同使用者資訊的 provider,將會是非常方便的。
  • Routing:許多路由方案會在內部使用 context 來維持當前 route,這能讓每個 link “知道” 現在是不是 active 的狀態。
  • Managing state:隨著專案成長,可能會有許多狀態存在於靠近 app 頂層的地方,通常會搭配 reducer 來管理複雜的狀態。
    ( 這部分在下一篇會有詳細的解說 )

Context 並不會受限於靜態的值,如果你在下一次渲染時傳遞了不同的值,React 將會更新該元件以下的所有內容,這也是為什麼 context 很常與 狀態搭配使用。

補充:但這也引伸出過於頻繁觸發重新渲染而導致的效能問題。

一般來說,如果在樹狀結構中,多個位於不同位置的遙遠元件需要同一筆資料,使用 context 將會是一個好方法。

--

--

Jamie Lo

正在往前端這個知識量爆炸的黑洞前行,內容多為平時的筆記整理,希望也能幫助到同樣在這條道路上前進的人💪