🌐

React Context APIの導入と代替ライブラリの比較

に公開

React でコンポーネント間の状態共有を実現する場合、Context API は最も基本的な選択肢の一つです。この記事では、Context API の基本的な概念から実装パターン、そして代替となる状態管理ライブラリとの比較まで解説します。

Context API の基本構成と動作原理

React Context API は、コンポーネントツリーを通じて明示的に props を渡さずに、データをコンポーネント間で共有するための仕組みです。

主要コンポーネント

Context API は 3 つの主要な部分から構成されています:

  • Context: データのコンテナ(createContextで作成)
  • Provider: 子コンポーネントにデータを提供するラッパーコンポーネント
  • Consumer: データを使用するコンポーネント(通常はuseContextフックで実装)

正しい実装方法(分割パターン)

Context API を効果的に使用するためには、次のようなファイル分割パターンが推奨されます:

// 1. contexts/group-context.tsx - コンテキストの定義のみを行う
import { createContext } from "react";

// コンテキストの型定義
type GroupContextType =
  | {
      groups: Group[];
      currentGroup: Group | null;
      setCurrentGroup: (group: Group) => void;
    }
  | undefined;

// コンテキストの作成
export const GroupContext = createContext<GroupContextType>(undefined);
// 2. contexts/group-provider.tsx - プロバイダーとロジックを実装
import { ReactNode, useState } from "react";
import { GroupContext } from "./group-context";

type GroupProviderProps = {
  children: ReactNode;
};

const initialState = {
  groups: [],
  currentGroup: null,
};

export const GroupProvider = ({ children }: GroupProviderProps) => {
  const [groups, setGroups] = useState([]);
  const [currentGroup, setCurrentGroup] = useState(null);

  // 実際の値を提供
  const value = {
    groups,
    currentGroup,
    setCurrentGroup,
  };

  return (
    <GroupContext.Provider value={value}>{children}</GroupContext.Provider>
  );
};
// 3. hooks/use-group.tsx - コンテキストにアクセスするためのカスタムフック
import { useContext } from "react";
import { GroupContext } from "../contexts/group-context";

export const useGroup = () => {
  const context = useContext(GroupContext);
  if (!context) {
    throw new Error("useGroup must be used within a GroupProvider");
  }
  return context;
};

ファイル分割の理由(Fast Refresh 問題)

このようにファイルを分割する主な理由は、React Fast Refresh の動作に関係しています。Fast Refresh は「コンポーネントのみをエクスポートするファイル」で最適に動作します。

コンテキストオブジェクトとプロバイダー(状態を持つ)が同じファイルにある場合、開発中にコードを変更すると状態がリセットされてしまう問題が発生します。これを避けるために、上記のようなファイル分割パターンが推奨されています。

実際の使用例

// アプリのルートでプロバイダーをセットアップ
function App() {
  return (
    <GroupProvider>
      <Layout>
        <YourComponents />
      </Layout>
    </GroupProvider>
  );
}

// 任意の子コンポーネントでコンテキストにアクセス
function GroupList() {
  const { groups, setCurrentGroup } = useGroup();

  return (
    <div className="group-list">
      {groups.map((group) => (
        <div
          key={group.id}
          className="group-item"
          onClick={() => setCurrentGroup(group)}
        >
          {group.name}
        </div>
      ))}
    </div>
  );
}

代替のグローバル状態管理ライブラリ

1. Redux

  • 特徴: 予測可能な状態管理、豊富なミドルウェア、強力なデバッグツール
  • 用途: 大規模アプリケーション、複雑な状態ロジック、履歴追跡が必要な場合
  • 備考: 学習曲線が急、ボイラープレートコードが多い
// Reduxの簡単な例
const counterReducer = (state = 0, action) => {
  switch (action.type) {
    case "INCREMENT":
      return state + 1;
    default:
      return state;
  }
};

const store = createStore(counterReducer);

2. Zustand

  • 特徴: シンプルな API、Hooks 中心設計、最小限のボイラープレート
  • 用途: 中小規模アプリ、Redux よりシンプルにしたい場合
  • 備考: 人気急上昇中、軽量でパフォーマンスが良い
// Zustandの簡単な例
const useStore = create((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
}));

function Counter() {
  const { count, increment } = useStore();
  return <button onClick={increment}>{count}</button>;
}

3. Recoil

  • 特徴: Facebook 製、アトムベースのアプローチ、React 的な設計思想
  • 用途: React に最適化された状態管理が必要な場合
  • 備考: まだ安定版ではない(2023 年現在)
// Recoilの簡単な例
const counterState = atom({
  key: "counterState",
  default: 0,
});

function Counter() {
  const [count, setCount] = useRecoilState(counterState);
  return <button onClick={() => setCount((c) => c + 1)}>{count}</button>;
}

4. Jotai

  • 特徴: プリミティブ API、軽量、Recoil にインスパイアされた設計
  • 用途: 細粒度の状態更新、バンドルサイズを小さくしたい場合
  • 備考: シンプルさが売り
// Jotaiの簡単な例
const countAtom = atom(0);

function Counter() {
  const [count, setCount] = useAtom(countAtom);
  return <button onClick={() => setCount((c) => c + 1)}>{count}</button>;
}

5. MobX

  • 特徴: リアクティブプログラミング、自動追跡と更新メカニズム
  • 用途: オブジェクト指向的アプローチが好みの場合
  • 備考: 宣言的な状態管理が特徴
// MobXの簡単な例
const counter = observable({
  value: 0,
  increment() {
    this.value += 1;
  },
});

const Counter = observer(() => (
  <button onClick={() => counter.increment()}>{counter.value}</button>
));

6. XState

  • 特徴: 有限状態機械に基づく状態管理
  • 用途: 複雑な UI フロー、多段階フォーム、状態遷移が複雑な場合
  • 備考: 視覚的なデバッグツールが便利
// XStateの簡単な例
const toggleMachine = createMachine({
  id: "toggle",
  initial: "inactive",
  states: {
    inactive: { on: { TOGGLE: "active" } },
    active: { on: { TOGGLE: "inactive" } },
  },
});

function Toggle() {
  const [state, send] = useMachine(toggleMachine);
  return (
    <button onClick={() => send("TOGGLE")}>
      {state.value === "inactive" ? "Off" : "On"}
    </button>
  );
}

Context API の長所・短所

Context API を使用するかどうかを検討する際に役立つ、主な長所と短所をまとめます。

長所

  • ネイティブ機能: React に組み込まれているため、追加ライブラリが不要
  • プロップドリリング解消: コンポーネント階層を通じて props を渡す必要がない
  • シンプルな API: 学習コストが低く、すぐに導入できる
  • 軽量: バンドルサイズへの影響が最小限

短所

  • パフォーマンスの最適化が難しい: Context 値が変更されると、すべての消費コンポーネントが再レンダリングされる
  • 大規模アプリでは再レンダリングが多発する可能性: 適切に設計しないと不要な再レンダリングが発生する
  • デバッグツールが限られている: 専用の状態管理ライブラリのような強力なデバッグツールがない
  • 複雑な状態ロジックには不向き: 非同期処理や複雑な状態更新には不向き

どれを採用すべきか

小さめのプロジェクトなら、React Context API, Jotai, Recoil あたりが採用されることが多い印象です。Redux は学習コストが高いのであまり採用されないイメージ。
 
 
 
 
以上です。

KA projects

Discussion