Sean's Blog

An image showing avatar

Hi, I'm Sean

這裡記錄我學習網站開發的筆記
歡迎交流 (ゝ∀・)b

LinkedInGitHub

使用 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

References