Back To Articles

使用 React Context 處理全域狀態

🧑🏻‍💻 海豹人 Sealman 📅 October 31, 2021

Article Image

為了避免 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>,就如同接下來範例的操作。

const AuthContext = React.createContext({
  isLoggedIn: false,
  onLogout: () => {},
});

export default AuthContext;

這裡所設定的值只是為了 IDE 的自動完成提示,跟狀態的預設值無關

Step 2. 提供 Context (Context.Provider)

要給某個元件使用 Context 的話,就透過 Provider 將這個 React Context 物件變成一個元件,這樣就能提供 Store 的資料給這個子元件使用了。本篇文章的範例中,我們想要讓 <MainHeader><Login><Home> 等元件都可以訪問到 AuthContext。

return (
  <AuthContext.Provider>
    <MainHeader isAuthenticated={isLoggedIn} onLogout={logoutHandler} />
    <main>
      {!isLoggedIn && <Login onLogin={loginHandler} />}
      {isLoggedIn && <Home onLogout={logoutHandler} />}
    </main>
  </AuthContext.Provider>
);

Step 3. 取用 Context (useContext Hook)

接下來,我們可以透過 useContext() 這個 Hook 來取得剛才建立好的 Context Store。但是在取用之前,我們必須為 Provider 加上 value 屬性以及設定預設值。

// 假設已經定義好了 isLoggedIn state 與 logoutHandler function...
return (
  <AuthContext.Provider
    value={{
      isLoggedIn: isLoggedIn,
      onLogout: logoutHandler,
    }}
  >
    <MainHeader />
    <main>
      {!isLoggedIn && <Login onLogin={loginHandler} />}
      {isLoggedIn && <Home onLogout={logoutHandler} />}
    </main>
  </AuthContext.Provider>
);

這裡所設定的 value 就是預設值,也會跟著 setState 做更新

在元件中使用 Context:

const Navigation = (props) => {
  const authCtx = useContext(AuthContext);

  return (
    <nav className={classes.nav}>
      <ul>
        {authCtx.isLoggedIn && (
          <li>
            <a href="/">Users</a>
          </li>
        )}
        {authCtx.isLoggedIn && (
          <li>
            <a href="/">Admin</a>
          </li>
        )}
        {authCtx.isLoggedIn && (
          <li>
            <button onClick={props.onLogout}>Logout</button>
          </li>
        )}
      </ul>
    </nav>
  );
};

export default Navigation;

Step 4. 提供為全域狀態 (Optional)

如果想要直接讓 Context 提供給全域使用,可以把 Provider 放到最外層,在 ReactDOM 渲染的時候就包覆整個 App。這麼做可以集中「狀態管理」的部分,也讓元件的邏輯集中,將不同的邏輯分離,算是一個滿推薦的做法。

在進入點 index.js 將 Provider 包在整個 App 外面,提供全域取用 Context。

ReactDOM.render(
  <AuthContextProvider
    value={{
      isLoggedIn: isLoggedIn,
      onLogout: logoutHandler,
    }}
  >
    <App />
  </AuthContextProvider>,
  document.getElementById("root"),
);

以下是一個範例,我們把所有 Context 相關的東西整理到一個元件中,最後匯出 Provider。

// <root>/src/store/auth-context.jsx

import React, { createContext, useState } from "react";

export const AuthContext = createContext({
  isLoggedIn: false,
  onLogout: () => {},
});

const AuthContextProvider = ({ children }) => {
  const [isLoggedIn, setIsLoggedIn] = useState(false);

  const loginHandler = (email, password) => {
    // handle login...
  };

  const logoutHandler = () => {
    // handle logout...
  };

  return (
    <AuthContext.Provider
      value={{
        isLoggedIn: isLoggedIn,
        onLogin: loginHandler,
        onLogout: logoutHandler,
      }}
    >
      {children}
    </AuthContext.Provider>
  );
};

export default AuthContextProvider;

可以看到這裡從 IDE 自動完成提示、useState、Function Handler、設定 Provider 預設值,上面提到的步驟幾乎全都做了,就集中在這個檔案裡面。

所以在最後要使用時,就只需要單純使用 <AuthContextProvider>,也不需要再這裡設定預設值了。

ReactDOM.render(
  <AuthContextProvider>
    <App />
  </AuthContextProvider>,
  document.getElementById("root"),
);

Recap

看完這篇文章,我們到底有什麼收穫呢?藉由本文可以理解到…

  • React Context
  • useContext Hook

References