🌐
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 は学習コストが高いのであまり採用されないイメージ。
以上です。
Discussion