我們知道,在 React 元件中,是無法直接水平跨元件傳遞 Props。若要,會需要先傳至父層元件,再傳遞下來。舉個例子:我們在建立電商網站時,可以想像在產品頁面,會包裹在瀏覽頁面、首頁等元件中。要讓如購物車的元件,接收到產品頁面傳遞的 Props,就需要隔著傳遞好幾層。這會造成兩個問題:
- 部分元件並不會使用 Props,卻仍然需要傳遞。
- 部分元件在重構後,可能導致 Props 忘記移除或難以辨識。
透過 Context,相較於使用 Redux,Context 可以提供的輕量級共享數據功能,並水平傳遞數據給其他元件使用。而 useContext,則提供了更容易理解的方式,來達到共享數據的目的。
什麼是 useContext? useContext 是 React 提供的一組 hook,讓我們可以在元件(Component)中訪問其他組件的內容(Context)。
而使用 useContext 帶來的優勢,是我們可以將數據,直接傳遞給組件樹當中的深層組件,而不用一層一層使用 Props 傳遞。
如何使用 Context 和 useContext?
在使用 Context 時,可以想像我們有一個負責提供資料的容器 Provider,以及負責接收資料的 Consumer。只要被容器 Provider 包裹到的元件,該元件與以下的元件,全部都能透過 Comsumer 或 useContext,接收到所需的資料。
以下就示範兩種 Context 的使用方式,同時也能從中發現,為何 useContext 會被發明出來:
- Provider + Consumer 的 Context 模式
- Provider + useContext 的模式
1.Provider + Consumer 的 Context 模式
在這種模式下,我們首先需要創建一個 Context 對象,然後使用 Context.Provider 在組件樹中,將我們想要提供數據的範圍,使用元件包裹起來。
通常我們會包裹在頂層,換句話說就是在 App.js 的元件裡;但當然也可以僅包裹在特定的組件子數。並在該串元件樹之中,使用 Consumer 來「消費」之中的 Context。使用流程如下:
- 建立資料夾與命名文件
- 創建 Provider
- 建立 Provider 元件
- 包裹欲接收元件樹
- 讓元件接收 value 資料
1.建立資料夾與命名文件
在元件資料夾裡,新增一個 store 或 state 的資料夾(通常慣用 store)。接著在 state 資料夾裡,新增想要管理的 props 檔案,使用 xxxx-context.js 等方式命名。同常命名要以小寫開頭,因為 context 並非元件。
2.創建 Provider
import react,並使用 const XxxxContext = React.createContext() 來創建 Props。其中,XxxxContext 本身並不是元件,而是一個「乘載元件的容器」。
3.建立 Provider 元件
此處為了讓 App.js 維持清晰,我們特意將 Provider 抽離出來,獨立使用 XxxxProvider 的元件,讓整個程式碼變得簡潔。當然,也可以直接將這段寫在 App.js 裡。
這樣寫也帶來了另一個優勢,所有處理 Context 的邏輯,全部都可以寫在這個 Provider 裡面。接著,使用export default XxxxProvider; 讓其他元件可以存取。
將我們希望接收範圍的元件,使用 AuthContext.Provider.../AuthContext.Provider 包裹。接著,將希望能夠傳出的值,傳入屬性 value 裡。此處使用物件或字串皆可。
此處未來會作為傳入一個更新的物件,因為 React 主要就是偵測傳入的 value 是否有變動,並據此重新渲染所有使用 Context 的元件。
4.包裹欲接收元件樹
接著在我們希望存取的物件,例如 Loggin 狀態希望可以讓所有人都存取,則在 App.js 引入。透過前面抽離的 Provider,我們可以簡單將整層的 Layout / 元件包裹起來。接著從 Layout 開始到往下所有的元件,全部都可以讀取 Context 提供的資料。
5.讓元件接收 value 資料
接著,在欲使用 Props 的元件,在 return 中使用 AuthContext.Comsumer 包裹起來,並在之中的元件,使用 {} 包裹,並將元件 HTML 包裹在匿名函式中。參數通常使用 ctx,就可以透過 ctx.totalAmount 來取值。
這個方法也是之前非 hook 的寫法,目前一樣可行。因為包裹 Consumer 的關係,容易導致 return 內容過於冗長。因此通常建議使用 useContext。
B. Provider + useContext 的模式
前面創建 Provider 的方式,和原先的 Provider + Consumer 的方式一模一樣,僅是後面 Consumer 的模式,被 useContext 所取代。以下一樣完整展示整個流程:
- 建立資料夾與命名文件
- 創建 Provider
- 建立 Provider 元件
- 包裹欲接收元件樹
- 讓元件接收 value 資料
1.建立資料夾與命名文件
在元件資料夾裡,一樣新增一個 store 或 state 的資料夾(通常慣用 store)。
2.創建 Provider
import react,並使用 const XxxxContext = React.createContext() 來創建 Props。其中,XxxxContext 本身並不是元件,而是一個「乘載元件的容器」。
小技巧:為了讓 IDE 更方便自動補齊,可以在 xxx-context.js 裡,在想要使用的屬性,放入空數值或空的匿名函式。
3.建立 Provider 元件
此處為了讓 App.js 維持清晰,我們特意將 Provider 抽離出來,獨立使用 XxxxProvider 的元件,讓整個程式碼變得簡潔。當然,也可以直接將這段寫在 App.js 裡。
這樣寫也帶來了另一個優勢,所有處理 Context 的邏輯,全部都可以寫在這個 Provider 裡面。接著,使用export default XxxxProvider; 讓其他元件可以存取。
以下是建立流程:
- 在 auth-context.js 中,創建希望能傳遞的屬性或方法,為了讓 IDE 自動補齊。
- 創建新元件 AuthContextProvider,並將 children 傳入並返回 CartContext.Provider{children}/CartContext.Provider。
- 在 App.js 中,將 render 改成 AuthContextProviderLayout //AuthContextProvider,使所有的物件全部都吃的到 Context。
- 將所有的 useEffect、useState、Handler 等,都放入 AuthContextProvider 元件中處理。
將預計傳入的值,放在 AuthContext.Provider value={{ totalAmount: totalAmount }}。注意,此處前面的 totalAmount 是 key,後面的值是傳入的變數。
4.包裹欲接收元件樹
接著在我們希望存取的物件,例如 TotalAmount 狀態希望可以讓所有人都存取,則在 App.js 引入。透過前面抽離的 Provider,我們可以簡單將整層的 Layout / 元件包裹起來。接著從 Layout 開始到往下所有的元件,全部都可以讀取 Context 提供的資料。
5.讓元件接收 value 資料
這一步,也是 useContext 最大的不同,以及所帶來的優勢–––程式碼簡潔。但並非所有的傳值,都需要使用 useContext 來修改。如果在傳值後子元件就能夠直接使用,則一樣使用 props 傳值就很夠用,不需特別改成 useContext 傳值。
在所有預計引用 props 的元件,使用 cont cartCtx = useContext(CartContext); 來獲得所有的 Props 和 Handler。因為各個模組間的變數是獨立的,所以所有的 Context 都可以取名為 cartCtx。
整個解析過程如下:
- 引入 context:import CartContext from "../../../store/cart-context";
- 傳入 useContext:const cartCtx = useContext(CartContext);
- 賦值解構物件:const { items } = cartCtx;
- 使用 value:const totalNumberOfItems = items.reduce(…);
其中,使用 reduce 的原因,主要是為了加總目前全部的 item amount,所以使用 reduce。而預設參數 0 的原因,則是為了避免在沒有資料時,reduce 會報錯 undefined。
使用 useContext 的必知必會
A. useContext 不適用於高頻轉換
因為 Context 也有另一個潛在的缺點:useContext 並不是設計給高頻轉換的功能,因此如果需要大量更新的狀態, useContext 並非好的選擇。若真的要使用跨元件的 props 溝通高頻狀態變更(每秒就更新一次或數次等),可以使用 Redux 來規劃。
B. useContext 的雙重包裹
在某些情況下,我們可能需要在應用程序中的多個層次上使用相同的 Context。在這種情況下,useContext 可以被雙重包裹,並且可以由多個 Context 提供者進行傳遞。範例如下:
C. useContext 僅能接收 Context
注意,useContext 只能接收 Context 對象作為參數,而不能接收其他 Hook 或函式。
使用 useContext 時,多考量不同面向
當我們使用到 useContext 時,通常就會想要拿來取代多層的 props 傳遞;但 useContext 其實也有一個潛在門檻,便是當我們使用 useContext 處理共用元件時,會導致共用元件僅能處理特定 Case。
舉個例子,如果我們有一個 Modal 總是會在輸入錯誤時,跳出錯誤訊息;那 Input 輸入錯誤和 Submit 輸入錯誤的行為,預設會輸出不同的內容。這時如果使用 useContext 管理狀態,就容易導致狀態處理變得單一。
但若在單一個 Context 中放入多個狀態,又容易導致意外的 bug,例如錯誤引入狀態等。因此建議若是在處理 UI 元件如 Modal 時,還是使用 props 傳值處理。