使用 React Context 處理全域狀態
為了避免 Props Drilling,React 提供了 Context API 幫助我們解決這樣的問題,本文會介紹 Context API 的基本用法,包含 createContext、Provider、Consumer、useContext 等概念。
React Context (Context API)
當 Props 從 Parent 傳到 Child,再往下傳給下一個 Child 時,這樣一層一層的傳遞鏈 (Props Drilling) 在大型專案上會相當複雜。舉例來說,「是否登入」這個狀態通常會使用在許多元件中,它會被用來驗證用戶是否登入,又或是購物車資料的狀態需要顯示在不同頁面。如果我們可以省略中間這些傳遞過程,直接從父元件傳遞 Props 給真正需要資料的子元件,那不是既方便又優雅嗎?
因此,我們可以使用 React Context 做到這件事情。
Step 1. 建置 Context (React.createContext)
首先,我們在 /src
下建立一個名為 store
的資料夾(這是常見的命名,並非絕對),然後新增一個 Context,在此範例中是用來管理登入授權相關的狀態。
可以看到這個 AuthContext
本身不是一個元件,它是一個用來包著元件的「物件」。如果想在某個子元件用到 Context,就用 AuthContext 包住那一個子元件。所以如果整個 App 都要用到,那也可以直接用 AuthContext 包住整個 <App>
,就如同接下來範例的操作。
1const AuthContext = React.createContext({ 2 isLoggedIn: false, 3 onLogout: () => {}, 4}); 5 6export default AuthContext; 7
這裡所設定的值只是為了 IDE 的自動完成提示,跟狀態的預設值無關
Step 2. 提供 Context (Context.Provider)
要給某個元件使用 Context 的話,就透過 Provider 將這個 React Context 物件變成一個元件,這樣就能提供 Store 的資料給這個子元件使用了。本篇文章的範例中,我們想要讓 <MainHeader>
、<Login>
、<Home>
等元件都可以訪問到 AuthContext。
1return ( 2 <AuthContext.Provider> 3 <MainHeader isAuthenticated={isLoggedIn} onLogout={logoutHandler} /> 4 <main> 5 {!isLoggedIn && <Login onLogin={loginHandler} />} 6 {isLoggedIn && <Home onLogout={logoutHandler} />} 7 </main> 8 </AuthContext.Provider> 9); 10
Step 3. 取用 Context (useContext Hook)
接下來,我們可以透過 useContext()
這個 Hook 來取得剛才建立好的 Context Store。但是在取用之前,我們必須為 Provider 加上 value
屬性以及設定預設值。
1// 假設已經定義好了 isLoggedIn state 與 logoutHandler function... 2return ( 3 <AuthContext.Provider 4 value={{ 5 isLoggedIn: isLoggedIn, 6 onLogout: logoutHandler, 7 }} 8 > 9 <MainHeader /> 10 <main> 11 {!isLoggedIn && <Login onLogin={loginHandler} />} 12 {isLoggedIn && <Home onLogout={logoutHandler} />} 13 </main> 14 </AuthContext.Provider> 15); 16
這裡所設定的 value 就是預設值,也會跟著 setState 做更新
在元件中使用 Context:
1const Navigation = (props) => { 2 const authCtx = useContext(AuthContext); 3 4 return ( 5 <nav className={classes.nav}> 6 <ul> 7 {authCtx.isLoggedIn && ( 8 <li> 9 <a href="/">Users</a> 10 </li> 11 )} 12 {authCtx.isLoggedIn && ( 13 <li> 14 <a href="/">Admin</a> 15 </li> 16 )} 17 {authCtx.isLoggedIn && ( 18 <li> 19 <button onClick={props.onLogout}>Logout</button> 20 </li> 21 )} 22 </ul> 23 </nav> 24 ); 25}; 26 27export default Navigation; 28
Step 4. 提供為全域狀態 (Optional)
如果想要直接讓 Context 提供給全域使用,可以把 Provider 放到最外層,在 ReactDOM 渲染的時候就包覆整個 App。這麼做可以集中「狀態管理」的部分,也讓元件的邏輯集中,將不同的邏輯分離,算是一個滿推薦的做法。
在進入點 index.js 將 Provider 包在整個 App 外面,提供全域取用 Context。
1ReactDOM.render( 2 <AuthContextProvider 3 value={{ 4 isLoggedIn: isLoggedIn, 5 onLogout: logoutHandler, 6 }} 7 > 8 <App /> 9 </AuthContextProvider>, 10 document.getElementById('root'), 11); 12
Step 5. 抽取出來建立一個 Context Store (Recommended)
以下是一個範例,我們把所有 Context 相關的東西整理到一個元件中,最後匯出 Provider。
1// <root>/src/store/auth-context.jsx 2 3import React, { createContext, useState } from 'react'; 4 5export const AuthContext = createContext({ 6 isLoggedIn: false, 7 onLogout: () => {}, 8}); 9 10const AuthContextProvider = ({ children }) => { 11 const [isLoggedIn, setIsLoggedIn] = useState(false); 12 13 const loginHandler = (email, password) => { 14 // handle login... 15 }; 16 17 const logoutHandler = () => { 18 // handle logout... 19 }; 20 21 return ( 22 <AuthContext.Provider 23 value={{ 24 isLoggedIn: isLoggedIn, 25 onLogin: loginHandler, 26 onLogout: logoutHandler, 27 }} 28 > 29 {children} 30 </AuthContext.Provider> 31 ); 32}; 33 34export default AuthContextProvider; 35
可以看到這裡從 IDE 自動完成提示、useState、Function Handler、設定 Provider 預設值,上面提到的步驟幾乎全都做了,就集中在這個檔案裡面。
所以在最後要使用時,就只需要單純使用 <AuthContextProvider>
,也不需要再這裡設定預設值了。
1ReactDOM.render( 2 <AuthContextProvider> 3 <App /> 4 </AuthContextProvider>, 5 document.getElementById('root'), 6); 7
Recap
看完這篇文章,我們到底有什麼收穫呢?藉由本文可以理解到…
- React Context
- useContext Hook