海豹人的第一個家

Sealman

Frontend Engineer / Taiwanese / Passion Comes From Mastery

使用 React Context 處理全域狀態

本文介紹 React Context 的基本用法,包含 createContext、Provider、Consumer、useContext 等觀念。

React Context (Context API)

當 Props 從 Parent 傳到 Child,再往下傳給下一個 Child 時,這樣一層一層的傳遞鏈在大型專案上會相當複雜,此時就可以使用 Context API 來處理。

例如:是否登入的狀態需要使用在許多場景,用來驗證用戶是否登入;亦或是購物車資料需要顯示於不同頁面。

如果我們可以省略中間的這些轉發的過程,直接從父組件傳遞 Props 給真正需要資料的子組件的話,不啻是一個方便且優雅的方式嗎?

因此,我們有了 React Context。

前置 1:React.createContext

在 src 下建立 store 資料夾,並新增一個 Context,它會用來管理與登入授權相關的狀態。

這個 AuthContext 本身並不是一個組件,它是用來包著組件的一個物件。

import React from 'react';

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

export default AuthContext;

如果在某個子元件會用到 Context,那就是用 AuthContext 包住那一個子元件。

如果整個 App 到處都會用到,那也可以選擇用 AuthContext 包住整個 App 的內容,如同接下來的範例。

前置 2:Context.Provider

在包的時候,我們會加上 .Provider 將這個 React Context 物件變成一個組件,這樣就能提供 Store 的資料給內層的子元件使用了。

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

如上所示,此時 MainHeader、Login、Home 等元件都可以訪問到 AuthContext 囉!

用法 1:使用 Context.Consumer

React.Context.Consumer 組件本身自帶一個 {child},並且是一個函式 (ctx) ⇒ {} 的形式。

參數 ctx 就是指向剛才 Store 裡面的資料,像是 { isLoggedIn: false },而函式的最後會 return 要存取這個資料的 JSX 程式碼。

import React from 'react';

import AuthContext from '../../store/auth-context';
import classes from './Navigation.module.css';

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

export default Navigation;

此時,如果網頁 Crash 了,那麼有可能是你同時使用了 Provider 與 Consumer,以下提供解決方式

解法一:只使用 Consumer 存取默認值就好

如果只想使用 Consumer 來存取默認值,也就是 { isLoggedIn: false },那麼使用 Consumer 時就不要同時用 Provider。

把剛才包覆的 Provider 先拿掉,這樣 Consumer 就能正常提供默認值。

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

解法二:同時使用 Consumer 與 Provider 時,Provider 要搭配 value

另一個方法是我們同時使用兩者,但是 Provider 要加上 value

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

除此之外,Provider 的值還可以做更改,用來設定載入時的初始值。

Example: Listen state (isLoggedIn) changes, and pass down to all components we consume this Context

return (
  <AuthContext.Provider
    value={{
      // -------- listen useState changes
      isLoggedIn: isLoggedIn,
    }}
  >
    <MainHeader onLogout={logoutHandler} />
    <main>
      {!isLoggedIn && <Login onLogin={loginHandler} />}
      {isLoggedIn && <Home onLogout={logoutHandler} />}
    </main>
  </AuthContext.Provider>
);

👍 用法 2: ⚓ useContext Hook

除了透過 Context.Consumer 來存取 Store 之外,我們也可以用 useContext 來取用 AuthContext。

useContext - React 官方文件

const value = useContext(MyContext);

useContext 會接收一個 context object(React.createContext 的回傳值)並回傳該 context 目前的值。Context 目前的值是取決於由上層 component 距離最近的 <MyContext.Provider>value prop。

要傳 String, Object, Function 都可以!

// Provider

return (
  <AuthContext.Provider
    value={{
      isLoggedIn: isLoggedIn,
      // Pass logoutHandler down to all components we consume this Context
      onLogout: logoutHandler,
    }}
  >
    <MainHeader />
    <main>
      {!isLoggedIn && <Login onLogin={loginHandler} />}
      {isLoggedIn && <Home onLogout={logoutHandler} />}
    </main>
  </AuthContext.Provider>
);

下面就是 useContext 的寫法,可以發現寫法真的簡潔優雅很多,大推 👍

// useContext

import React, { useContext } from 'react';

import AuthContext from '../../store/auth-context';
import classes from './Navigation.module.css';

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

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

export default Navigation;
// Use Navigation Component

const Navigation = () => {
  const ctx = useContext(AuthContext);

  return (
    <nav className={classes.nav}>
      <ul>
        {/* omit above code */}
        {ctx.isLoggedIn && (
          <li>
            <button onClick={ctx.onLogout}>Logout</button>
          </li>
        )}
      </ul>
    </nav>
  );
};

Props vs. Context

這樣看起來,有些時候使用 Props 還是 Context 好像都能達到傳遞的效果,那它們兩個的使用時機該怎麼決定呢?

其實在一般情況下,我們都使用 Props 居多,這樣才能讓組件具有「複用性」。
因為如果使用 Context,那麼該組件就會永遠是那一個功能,便會失去它的複用性。

所以通常只有當我們要轉發的資料,需要越過很多元件時,或是該元件有一個明確、具體的作用時,我們才會使用到 Context。

Better IDE support and autocomplete

使用 Context 時,建議只要有調用到,就應該要加進 Context 的「默認值」,例如:函式可以定義一個 Dummy Function,如此一來,像是我們在輸入 ctx. 時,就會出現輸入提示 onLogout

import React from 'react';

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

export default AuthContext;

透過自定義的 Context Provider 提供 Global 狀態

如果是 Global 狀態,其實可以放到更外層,以便讓 App.js 的程式碼更單純,也就是單純用來渲染 Web App 主體而已。

做法就是我們把 Context Provider 寫在 index.js 當中,在 ReactDOM.render 的時候就包覆 App。

這麼做可以更集中狀態管理的部分,也能讓組件的邏輯集中,將不同的邏輯分離,算是一個滿推薦的做法。

// index.js

export const AuthContextProvider = (props) => {
  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,
      }}
    >
      {props.children}
    </AuthContext.Provider>
  );
};
ReactDOM.render(
  <AuthContextProvider>
    <App />
  </AuthContextProvider>,
  document.getElementById('root')
);

Recap

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

  • React Context
  • useContext Hook

References