React の状態管理パターン比較 – useState vs Context API vs Zustand
React での状態管理の重要性
React アプリケーション開発で状態管理は、プロジェクト成功の鍵になります。「同じ状態を複数のコンポーネントから参照したい」「深い階層を通じて状態を渡すのが大変」といった React 開発者の悩みはありませんか?
React には複数の状態管理パターンがあり、それぞれ異なるユースケースに最適化されています。今回は、useState、Context API、Zustand の3つのアプローチを比較し、React での状態管理でどのパターンを選ぶべきかをまとめておきたいと思います。
対象となる方
- React の基本的な使い方は知っている方
- 状態管理の方法について迷っている方
- プロジェクト規模に応じた選択肢を知りたい方
※ このドキュメントは React 18 以上で書いていきます。
useState:最も基本的な状態管理
useState の仕組みと限界
useState は React の基本的なフックで、コンポーネント内で状態を保持します。シンプルですが、いくつかの制限があります。
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>
+1
</button>
</div>
);
}
useState の主な問題点:
- Props Drilling:複数の階層を通じて状態を渡す必要がある
- 単一コンポーネント内のみ:兄弟コンポーネント間での状態共有が困難
- 複数の状態管理:複数の useState が増えると管理が複雑になる
- リレンダリング最適化:親の再レンダリングが子に伝播する
useState の適用場面
- 単一コンポーネント内のみで使う状態
- フォームの入力値、トグル、モーダルの開閉など局所的な状態
- 小規模プロジェクト・プロトタイプ開発
- UI の一時的な状態管理
Context API:複数コンポーネント間での状態共有
Context API の基本
Context API は、コンポーネントツリー全体で状態を共有するための React 標準機能です。Props Drilling を避けられるため、中規模プロジェクトに最適です。
import { createContext, useState } from 'react';
const ThemeContext = createContext();
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}
Context API を使用するコンポーネント:
import { useContext } from 'react';
import { ThemeContext } from './ThemeProvider';
function Header() {
const { theme, setTheme } = useContext(ThemeContext);
return (
<header style={{ background: theme === 'dark' ? '#333' : '#fff' }}>
<button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>
Toggle Theme
</button>
</header>
);
}
Context API のメリット・デメリット
メリット:
- React 標準機能なので外部ライブラリ不要
- Props Drilling を完全に排除できる
- シンプルな学習曲線
- 小〜中規模プロジェクトに十分
デメリット:
- Context の値が変更されると、すべてのコンシューマーが再レンダリングされる
- パフォーマンス最適化が必要になることがある
- 大規模状態管理には向かない
- デバッグが複雑になる可能性がある
Zustand:軽量でシンプルな状態管理
Zustand の概要と利点
Zustand は、Redux よりもシンプルで、Context API よりもパフォーマンスに優れた外部ライブラリです。バンドルサイズが小さく、学習曲線も緩いです。
import { create } from 'zustand';
const useCountStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
reset: () => set({ count: 0 }),
}));
// コンポーネントで使用
function Counter() {
const count = useCountStore((state) => state.count);
const increment = useCountStore((state) => state.increment);
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>+1</button>
</div>
);
}
Zustand の特徴
- バンドルサイズが非常に小さい(約 2.5KB)
- ボイラープレートコードが少ない
- 必要な状態のみをサブスクライブするため、パフォーマンスが良い
- Redux DevTools 対応
- TypeScript サポートが優れている
3つのアプローチの比較表
| 項目 | useState | Context API | Zustand |
| 学習曲線 | 最も簡単 | 簡単 | やや簡単 |
| バンドルサイズ | 0KB(組込) | 0KB(組込) | ~2.5KB |
| パフォーマンス | 中程度 | 中程度 | 優秀 |
| 適用規模 | 小規模 | 中規模 | 中〜大規模 |
| ボイラープレート | 少ない | 中程度 | 少ない |
| デバッグ性 | 優秀 | 中程度 | 優秀 |
選択基準:プロジェクト規模別ガイド
小規模プロジェクト(単ページアプリケーション)
useState で十分です。外部ライブラリを追加する必要はありません。複数ページの場合でも、各ページ独立の状態管理なら useState で対応できます。
中規模プロジェクト(複数ページ、複数の共有状態)
Context API が推奨されます。テーマ設定、認証情報、言語設定など、アプリケーション全体で共有する状態が少ない場合は Context API が最適です。
大規模プロジェクト(複雑な状態管理、複数チーム開発)
Zustand の使用を検討しましょう。Redux も選択肢ですが、Zustand の方がセットアップが簡単で、学習曲線が緩いです。状態が極めて複雑な場合のみ Redux を検討します。
実装例:同じ機能を各パターンで実装
ユーザー情報の状態管理
ユーザーログイン情報をアプリケーション全体で共有する例です。
Context API での実装:
const UserContext = createContext();
export function UserProvider({ children }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(false);
const login = async (email, password) => {
setLoading(true);
try {
const response = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
});
const data = await response.json();
setUser(data.user);
localStorage.setItem('user', JSON.stringify(data.user));
} finally {
setLoading(false);
}
};
const logout = () => {
setUser(null);
localStorage.removeItem('user');
};
return (
<UserContext.Provider value={{ user, loading, login, logout }}>
{children}
</UserContext.Provider>
);
}
Zustand での実装:
const useUserStore = create((set) => ({
user: null,
loading: false,
login: async (email, password) => {
set({ loading: true });
try {
const response = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
});
const data = await response.json();
set({ user: data.user });
localStorage.setItem('user', JSON.stringify(data.user));
} finally {
set({ loading: false });
}
},
logout: () => {
set({ user: null });
localStorage.removeItem('user');
}
}));
パフォーマンス最適化のベストプラクティス
Context API の場合
- 状態を分割し、複数の Context に分ける
useMemoを使って Context の値をメモ化するuseCallbackを使ってコールバック関数をメモ化する- 必要に応じて
React.memoでコンポーネントをラップする
Zustand の場合
- セレクター関数で必要な状態のみをサブスクライブ
- 浅い比較により不要なレンダリングを自動回避
- 状態を論理的にグループ化してストアを分割
まとめ
React での状態管理の選択は、プロジェクト規模、チーム規模、要件により異なります。
- useState:シンプルな小規模プロジェクトに最適
- Context API:React 標準機能で中規模プロジェクトに推奨
- Zustand:軽量で高パフォーマンス、大規模プロジェクトに最適
各パターンのメリット・デメリットを理解し、プロジェクトの要件に応じて最適なアプローチを選びましょう。必要以上に複雑な状態管理ツールを導入する必要はありません。まずは シンプルに始めて、必要に応じてスケールアップするのが賢明です。
