本篇筆記摘錄自官方文件 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 大小一致:
目前我們是將level
prop 分別傳入<Heading>
:
<Section>
<Heading level={3}>About</Heading>
<Heading level={3}>Photos</Heading>
<Heading level={3}>Videos</Heading>
</Section>
將level
prop移至<Section>
元件會是一個比較好的做法,此方法可以確保相同的 section 裡,headings 擁有相同的大小:
<Section level={3}>
<Heading>About</Heading>
<Heading>Photos</Heading>
<Heading>Videos</Heading>
</Section>
但是<Heading>
要如何取得最接近<Section>
裡的level
呢?我們必須要有一些方法,讓子元件可以向該樹狀結構以上的某個地方 “請求” 資料。
單靠 props 是沒有辦法完成的,我們可以透過下面三個步驟來使用 context:
- 創建 context
( 由於是用來存放 heading level,我們可以命名為LevelContext
) - 在需要資料的元件使用 context
(Heading
將會使用LevelContext
) - 擁有指定資料的元件提供 ( 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 }) {
// ...
}
請移除level
prop,改為使用剛剛匯入的LevelContext
:
export default function Heading({ children }) {
const level = useContext(LevelContext);
// ...
}
useContext
是一個 Hook,就跟useState
與useReducer
一樣,你只能在元件的最頂層呼叫它們,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>
改由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
傳遞level
prop,它會透過請求父層中最接近的Section
來 “計算” 出 heading 大小:
- 傳遞
level
prop 到<Section>
Section
將它的 children 包進<LevelContext.Provider value={level}>
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中,不同的屬性 ( 像是color
與background-color
) 不會覆蓋彼此,同樣地,不同的 React contexts 也不會覆寫彼此,每一個你用createContext()
創造出來的 context 都是彼此分開的,同一個元件使用或 provider 多個不同的 context 是完全沒有問題的。
Before you use context
context 是一個誘人使用的東西,這意味這它容易被過度使用。
需要傳遞 props 到一些較深層級處,不代表就必須使用 context。
這裡有一些你在使用 context 前可以考慮的選擇:
- 從傳遞 props 開始,如果元件並不瑣碎,將多個 props 傳入多個元件並不是不行的,這個方法能讓元件的資料來源清楚明瞭,利於維護。
- 提取元件並使用來
children
傳遞內容,如果有一筆資料通過多層中間元件,而那些中間的元件並不需要這份資料 ( 只是將資料傳到更遠處 ),這通常意味著你忘記提取元件。
舉例來說,相較於將posts
prop 傳遞給不會直接使用到的元件<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 將會是一個好方法。