Reactでグローバルな状態管理を行う方法(Context APIの活用)

以前作成したメモアプリに「ログインボタン」を実装し、グローバルなstateを扱う最もシンプルな方法として、React の機能であるContext(コンテクスト)を学びました。

knkkojt6.hatenablog.com

概説

useState()によって管理される値はローカルなstateと呼ばれ、その値は特定のコンポーネント内でしか利用できない。 しかし、複数のコンポーネントからその値を参照したい場合、propsを大量に渡す必要が出てくる。このような状況を避けるために、グローバルな状態管理が有用。

Contextとは

コンポーネントツリー全体でデータを共有するための方法。
複数のコンポーネントにprops経由で渡す手間が省ける。
アプリケーション全体で必要とされる情報(認証情報、テーマ設定など)を扱う際に便利。

Contextの作成

以下のようにcreateContext()を使用してContextを作成。

import { createContext } from 'react';

export const MyContext = createContext(defaultValue);
Providerの使用

Providerは、value属性を用いて子孫コンポーネントにコンテクストの値を供給。 一方で、子孫コンポーネントuseContext()フックを使用してこの値にアクセスできる。

<MyContext.Provider value={/* some value */}>
  {/* children */}
</MyContext.Provider>

今回の要件

  • 未ログイン時はログインボタンを、ログイン時はログアウトボタンを表示する。
  • 未ログイン時はメモの閲覧のみ可能で、ログインするとメモの追加/更新/削除ができる。

※ UI制御の練習のため、ID/PasswordやOAuth等を利用した実際の認証機能を実装はなし。
ログインボタンを押せばログイン状態になり、ログアウトボタンを押せばログアウト状態となるような「見かけ上のログイン機能」。新規アカウント作成等の機能もなし。

実装

可読性と保守性を高めるために、ReactのJSX構文を含むファイルは.jsxであるべき。

1. ログイン状態管理用のContextとカスタムフックを作成

新規でsrc/AuthContext.jsxを作成し、そこにログイン状態を管理するためのAuthContextを定義。
ここでは、Custom HookのuseLoginも作成。

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

const AuthContext = createContext();

export function AuthProvider({ children }) {
  const [loggedIn, setLoggedIn] = useState(false);

  const login = () => setLoggedIn(true);
  const logout = () => setLoggedIn(false);

  return (
    <AuthContext.Provider value={{ loggedIn, login, logout }}>
      {children}
    </AuthContext.Provider>
  );
}

export function useLogin() {
  return useContext(AuthContext);
}
2. App.jsでログインプロバイダーを使用

src/App.jsで作成したAuthProviderをラップすることで、全コンポーネントでログイン状態を共有できるようにする。

import { AuthProvider } from "./AuthContext.jsx"; // 追加
...
function App() {
  return (
    <AuthProvider>
      {/* ラップする */}
    </AuthProvider>
  );
}

export default App;
3. ログイン/ログアウトボタンの実装

ログイン/ログアウトボタンを追加。

src/components/MemoList.jsx

import { useLogin } from "../AuthContext.jsx"; // 追加

function MemoList({ memos, onMemoSelect, onMemoAdd }) {
  const { loggedIn, login, logout } = useLogin(); // 追加

  return (
    <>
      <div className="login-button">
        <button onClick={loggedIn ? logout : login}>
          {loggedIn ? "ログアウト" : "ログイン"}
        </button>
      </div>
      {memos.map((memo) => (
        <div
          key={memo.id}
          onClick={() => onMemoSelect(memo.id)}
          className="memo-item"
        >
          {memo.text.split("\n")[0]}
        </div>
      ))}
      {loggedIn && <button onClick={onMemoAdd}>+</button>} {/* ログイン時のみ表示 */}
    </>
  );
}

export default MemoList;
4. ログイン時のみ特定のUIを表示

メモの編集と削除をログイン状態に応じて制御。

src/components/MemoDetail.jsx

import { useLogin } from '../AuthContext'; // 追加

function MemoDetail({ memo, onMemoSave, onMemoDelete }) {
  const { loggedIn } = useLogin(); // 追加
  ...

  return (
    <>
      <textarea
        value={memoText}
        onChange={(e) => setMemoText(e.target.value)}
        readOnly={!loggedIn} // ログインしていない場合は読み取り専用
      />
      {loggedIn && (
        <>
          <button onClick={handleSave}>編集</button>
          <button className="delete-button" onClick={onMemoDelete}>
            削除
          </button>
        </>
      )}
    </>
  );
}

export default MemoDetail;

まとめ✏️

今回は見かけ上でしたが、ログイン/ログアウト機能を割と簡単に実装できました。 Contextでアプリ全体で状態を共有できるので、コードの冗長性を大幅に削減することができました。 次のステップとしては、実際の認証機能(JWT, OAuthなど)や新規アカウント作成機能も踏まえて学習していきたいです。

参考資料
コンテクストで深くデータを受け渡す
useContext
createContext
React Context を export するのはアンチパターンではないかと考える
カスタムフックで Context をエクスポートしないようリファクタリングする