使用 Context 深度傳遞資料
通常,你會透過 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 有許多用途。這裡有一個例子。考慮這個Heading元件,它接受一個level來決定其大小:
假設你希望同一個Section內的多個標題始終具有相同的大小:
目前,您需要單獨將level屬性傳遞給每個<Heading>:
如果能將 level屬性傳遞給<Section> 元件,並從 <Heading>中移除它,那就更好了。這樣您就能確保同一區段中的所有標題都具有相同的大小:
但是 <Heading> 元件如何知道其最近的 <Section>的層級呢?這需要某種方式讓子元件能夠向樹狀結構中上方的某處「請求」資料。
僅靠屬性無法做到這一點。這就是上下文發揮作用的地方。您將分三個步驟進行:
- 建立一個上下文。(您可以將其稱為
LevelContext,因為它用於標題層級。) - 從需要資料的元件中使用該上下文。(
Heading將使用LevelContext。) - 從指定資料的元件中提供該上下文。(
Section將提供LevelContext。)
上下文讓父元件——即使是遙遠的父元件!——能夠向其內部的整個樹狀結構提供某些資料。
在鄰近的子元件中使用上下文


在遙遠的子元件中使用上下文


步驟 1:建立上下文
首先,您需要建立上下文。您需要從一個檔案中匯出它,以便您的元件可以使用它:
傳給 createContext的唯一參數是預設值。這裡的1 指的是最大的標題層級,但你可以傳遞任何類型的值(甚至是物件)。你將在下一步看到預設值的重要性。
步驟 2:使用 Context
從 React 匯入useContextHook 以及你的 context:
目前,Heading元件從 props 讀取level:
請移除 levelprop,並從你剛剛匯入的 context(LevelContext)讀取值:
useContext是一個 Hook。就像useState 和 useReducer一樣,你只能在 React 元件內部(而非迴圈或條件中)立即呼叫 Hook。useContext告訴 React,Heading 元件想要讀取 LevelContext。
現在 Heading 元件沒有 levelprop 了,你不再需要像這樣在 JSX 中將 level prop 傳遞給Heading:
更新 JSX,讓 Section 來接收它:
提醒一下,這是你試圖運作的標記結構:
請注意這個範例目前還無法正常運作!所有標題都顯示相同的大小,這是因為雖然你已經使用了上下文,但尚未提供它。React 不知道該從哪裡獲取它!
如果你沒有提供上下文,React 將會使用你在前一步驟中指定的預設值。在這個範例中,你將1作為參數傳遞給createContext,因此useContext(LevelContext)會回傳1,導致所有標題都被設定為<h1>。讓我們透過讓每個Section提供自己的上下文來解決這個問題。
步驟 3:提供上下文
目前Section元件會渲染其子元件:
用一個上下文提供者包裹它們,以向它們提供LevelContext:
這告訴 React:「如果這個<Section>內部的任何元件請求LevelContext,就給它們這個level。」元件將會使用 UI 樹中位於其上方的最近一個<LevelContext>的值。
這與原始程式碼的結果相同,但你不需要將level屬性傳遞給每個Heading元件!相反地,它透過詢問上方最近的Section來「推斷」出它的標題層級:
- 您將一個
levelprop 傳遞給<Section>。 Section將其子元件包裹在<LevelContext value={level}>中。Heading使用useContext(LevelContext)向上詢問最近的LevelContext值。
在同一個元件中使用和提供上下文
目前,您仍然需要手動指定每個區段的level:
由於上下文允許您從上層元件讀取資訊,每個 Section都可以讀取上層Section 的 level,並自動向下傳遞level + 1。以下是實現方式:
經過此修改,您 也無需將levelprop 傳遞給<Section> 或 <Heading>:
現在 Heading 和 Section都會讀取LevelContext 來判斷它們的「深度」。而 Section會將其子元件包裹在LevelContext中,以指定其內部任何內容都處於「更深」的層級。
注意
此範例使用標題層級是因為它們直觀地展示了嵌套元件如何覆蓋上下文。但上下文對於許多其他用例也很有用。您可以向下傳遞整個子樹所需的任何資訊:當前顏色主題、當前登入的使用者等等。
上下文會穿過中間元件
您可以在提供上下文的元件和使用它的元件之間插入任意數量的元件。這包括內建元件如 <div>以及您可能自行建構的元件。
在此範例中,相同的Post元件(帶有虛線邊框)在兩個不同的嵌套層級中渲染。請注意,其中的<Heading> 會自動從最近的 <Section>獲取其層級:
您無需為此做任何特殊處理。Section為其內部的樹狀結構指定了上下文,因此您可以在任何地方插入<Heading>,它都會獲得正確的大小。請在上面的沙盒中試試看!
上下文讓您可以編寫「適應其周圍環境」的元件,並根據它們在哪裡(或者換句話說,在哪個上下文中)被渲染來以不同方式顯示自己。
上下文的工作原理可能會讓您想起CSS 屬性繼承。在 CSS 中,您可以為一個color: blue 指定 <div>,並且其內部的任何 DOM 節點,無論多深,都會繼承該顏色,除非中間有其他 DOM 節點用color: green覆蓋它。類似地,在 React 中,覆蓋來自上層的某些上下文的唯一方法是將子元件包裝到具有不同值的上下文提供者中。
在 CSS 中,像 color 和 background-color這樣的不同屬性不會互相覆蓋。您可以將所有<div>的color設為紅色,而不影響background-color。類似地,不同的 React 上下文不會互相覆蓋。 您使用 createContext()建立的每個上下文都與其他上下文完全分離,並將使用和提供該特定上下文的元件綁定在一起。一個元件可以毫無問題地使用或提供許多不同的上下文。
在使用上下文之前
上下文非常誘人!然而,這也意味著它很容易被過度使用。僅僅因為您需要將某些 props 傳遞多層深度,並不意味著您應該將該資訊放入上下文中。
在使用上下文之前,您應該考慮以下幾種替代方案:
- 首先從傳遞 props 開始。如果您的元件不是簡單的,將十幾個 props 向下傳遞經過十幾個元件並不罕見。這可能感覺很繁瑣,但它讓哪些元件使用哪些資料變得非常清晰!維護您程式碼的人會很高興您透過 props 明確了資料流。
- 提取元件並將 JSX 作為 children 傳遞給它們。 如果您將某些資料傳遞給許多不使用該資料(僅將其進一步向下傳遞)的中間元件層,這通常意味著您忘記了沿途提取一些元件。例如,您可能將像
posts這樣的資料 props 傳遞給不直接使用它們的可視元件,例如<Layout posts={posts} />。相反,讓Layout接受children作為 prop,並渲染<Layout><Posts posts={posts} /></Layout>。這減少了指定資料的元件與需要資料的元件之間的層數。
如果這兩種方法都不適合您,請考慮使用上下文。
上下文的用例
- 主題設定:如果你的應用程式允許使用者改變外觀(例如深色模式),你可以在應用程式頂層放置一個 context provider,並在需要調整視覺外觀的元件中使用該 context。
- 當前帳戶:許多元件可能需要知道當前登入的使用者。將其放入 context 中可以方便地在樹狀結構的任何地方讀取它。有些應用程式還允許你同時操作多個帳戶(例如以不同使用者身份發表評論)。在這些情況下,將部分 UI 包裝到具有不同當前帳戶值的巢狀 provider 中會很方便。
- 路由:大多數路由解決方案在內部使用 context 來保存當前路由。這就是每個連結如何「知道」自己是否處於活動狀態。如果你建立自己的路由器,你可能也會想這樣做。
- 管理狀態:隨著應用程式的增長,你可能會在應用程式頂層附近累積大量狀態。下方許多遙遠的元件可能想要改變它。通常會將 reducer 與 context 結合使用來管理複雜的狀態,並將其傳遞給遙遠的元件,而不會帶來太多麻煩。
Context 不限於靜態值。如果你在下一次渲染時傳遞不同的值,React 將會更新下方所有讀取它的元件!這就是為什麼 context 經常與狀態結合使用。
一般來說,如果樹狀結構不同部分的遙遠元件需要某些資訊,這是一個很好的跡象,表明 context 將對你有所幫助。
總結
- Context 允許一個元件向其下方的整個樹狀結構提供某些資訊。
- 要傳遞 context:
- 使用
export const MyContext = createContext(defaultValue)建立並匯出它。 - 在任何子元件中,無論層級多深,都可以透過
useContext(MyContext)Hook 來讀取它。 - 將子元件包裝到
<MyContext value={...}>中,以便從父元件提供它。
- 使用
- Context 會穿過中間的任何元件。
- Context 讓你編寫能夠「適應其周圍環境」的元件。
- 在使用 context 之前,嘗試傳遞 props 或將 JSX 作為
children傳遞。
嘗試一些挑戰
Challenge 1 of 1:使用 Context 替換屬性鑽取 #
在這個範例中,切換核取方塊會改變傳遞給每個 imageSize 元件的 <PlaceImage> 屬性。核取方塊的狀態儲存在頂層的 App 元件中,但每個 <PlaceImage> 都需要知道這個狀態。
目前,App 將 imageSize 傳遞給 List,然後 List 將其傳遞給每個 Place,接著 Place 再將其傳遞給 PlaceImage。請移除 imageSize 屬性,改為從 App 元件直接傳遞給 PlaceImage。
您可以在 Context.js 中宣告 context。
