React の状態管理パターン比較 – useState vs Context API vs Zustand
React での状態管理の重要性
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>
);
}
シンプルで分かりやすい一方で、以下の問題があります:
- Props Drilling:複数の階層を通じて状態を渡す必要がある
- 単一コンポーネント内のみ:兄弟コンポーネント間での状態共有が困難
- 複数の状態管理:複数の useState が増えると管理が複雑になる
useState の適用場面
- 単一コンポーネント内のみで使う状態
- フォームの入力値、トグルなど局所的な状態
- 小規模プロジェクト・プロトタイプ
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>
);
}
function useTheme() {
return useContext(ThemeContext);
}
// 使用例
function Header() {
const { theme, setTheme } = useTheme();
return (
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
Toggle: {theme}
</button>
);
}
Context API のメリット・デメリット
| メリット | デメリット |
| React 標準機能(外部ライブラリ不要) | パフォーマンス最適化が必要(不必要な再レンダリング) |
| Props Drilling が不要 | Context 分割が多くなると管理が複雑 |
| 小〜中規模プロジェクトに最適 | 複雑な状態遷移には向かない |
Context API の適用場面
- テーマ(ライト/ダーク)、言語設定などのグローバル状態
- 認証情報(ログインユーザー)の共有
- 中規模プロジェクト
- シンプルで頻繁に変わらない状態
Zustand:シンプルで高速な状態管理ライブラリ
Zustand の概要と利点
Zustand は、Redux の複雑さを軽減した、シンプルで高速な状態管理ライブラリです。最小限のボイラープレートで、柔軟な状態管理が実現できます。
npm install zustand
import { create } from 'zustand';
const useCountStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
}));
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 のメリット・デメリット
| メリット | デメリット |
| シンプルな API、ボイラープレートが少ない | 外部ライブラリ依存 |
| 高速(不必要な再レンダリングなし) | Redux Devtools との連携は手動設定 |
| DevTools サポート | 学習コスト(Redux より低い) |
| 複雑な状態遷移に対応 | 状態が複数ストアに分散する可能性 |
Zustand の適用場面
- 複雑な状態遷移が必要なアプリケーション
- パフォーマンスが重要なプロジェクト
- 複数ページ・複数セクションで共有する状態
- 大規模プロジェクト
3つのアプローチの比較表
| 観点 | useState | Context API | Zustand |
| 学習コスト | 最低 | 低 | 中 |
| ボイラープレート | 最小 | 中 | 最小 |
| パフォーマンス | 制限あり | 要最適化 | 優秀 |
| スケーラビリティ | 低 | 中 | 高 |
| DevTools サポート | なし | なし | あり |
| 外部ライブラリ | 不要 | 不要 | 必要 |
実装例:同じ機能を3パターンで実装
例:ユーザー認証情報の管理
① useState のパターン(局所的な使用)
function LoginForm() {
const [email, setEmail] = useState('');
const [isLoading, setIsLoading] = useState(false);
const handleLogin = async () => {
setIsLoading(true);
// ログイン処理
setIsLoading(false);
};
return (
<form onSubmit={handleLogin}>
<input value={email} onChange={(e) => setEmail(e.target.value)} />
<button disabled={isLoading}>{isLoading ? '登録中...' : 'ログイン'}</button>
</form>
);
}
② Context API のパターン(グローバル状態共有)
const AuthContext = createContext();
function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const login = async (email, password) => {
setIsLoading(true);
const response = await fetch('/api/login', { method: 'POST', body: JSON.stringify({ email, password }) });
const userData = await response.json();
setUser(userData);
setIsLoading(false);
};
return (
<AuthContext.Provider value={{ user, isLoading, login }}>
{children}
</AuthContext.Provider>
);
}
function LoginForm() {
const { isLoading, login } = useContext(AuthContext);
const [email, setEmail] = useState('');
return (
<form onSubmit={() => login(email, '')}>
<input value={email} onChange={(e) => setEmail(e.target.value)} />
<button disabled={isLoading}>{isLoading ? '登録中...' : 'ログイン'}</button>
</form>
);
}
③ Zustand のパターン(高速で最適化)
const useAuthStore = create((set) => ({
user: null,
isLoading: false,
login: async (email, password) => {
set({ isLoading: true });
const response = await fetch('/api/login', { method: 'POST', body: JSON.stringify({ email, password }) });
const userData = await response.json();
set({ user: userData, isLoading: false });
},
}));
function LoginForm() {
const isLoading = useAuthStore((state) => state.isLoading);
const login = useAuthStore((state) => state.login);
const [email, setEmail] = useState('');
return (
<form onSubmit={() => login(email, '')}>
<input value={email} onChange={(e) => setEmail(e.target.value)} />
<button disabled={isLoading}>{isLoading ? '登録中...' : 'ログイン'}</button>
</form>
);
}
プロジェクト規模別選択ガイド
小規模プロジェクト(コンポーネント数 < 20)
推奨:useState のみ
- 各コンポーネントで独立した状態を管理
- 必要に応じて状態をリフトアップ
- 外部ライブラリ不要
中規模プロジェクト(コンポーネント数 20-100)
推奨:Context API + useState
- テーマ、言語、認証などグローバル状態は Context
- ページごとの状態は useState
- Props Drilling を避ける
大規模プロジェクト(コンポーネント数 > 100)
推奨:Zustand または Redux
- 複雑な状態遷移に対応
- DevTools でデバッグ
- パフォーマンス最適化が容易
よくある問題と解決策
Context API で不必要な再レンダリングが発生する場合
context の値が毎回新しいオブジェクトになると、すべての購読者が再レンダリングされます。useMemo で値をメモ化します。
function Provider({ children }) {
const [count, setCount] = useState(0);
// ✅ 値をメモ化
const value = useMemo(() => ({ count, setCount }), [count]);
return (
<CountContext.Provider value={value}>
{children}
</CountContext.Provider>
);
}
Zustand で複数のストアを管理する場合
機能ごとにストアを分割し、必要に応じてストア間のアクションから別ストアの状態を更新します。
const useUserStore = create((set) => ({
user: null,
setUser: (user) => set({ user }),
}));
const useNotificationStore = create((set) => ({
message: '',
show: (msg) => set({ message: msg }),
clear: () => set({ message: '' }),
}));
// ユーザー設定時に通知を表示
function useLoginHandler() {
const setUser = useUserStore((state) => state.setUser);
const show = useNotificationStore((state) => state.show);
return async (email, password) => {
const user = await login(email, password);
setUser(user);
show(`${user.name}さん、ようこそ!`);
};
}
まとめ
以上で React の状態管理パターン比較を終えたいと思います。
React の状態管理は「銀の弾」ではなく、プロジェクト規模・要件に応じて選択することが重要です。React での状態管理として、小規模なら useState、中規模なら Context API、大規模なら Zustand といった具合に、適切なツールを選ぶことで、保守性とパフォーマンスを両立させられます。
「React での状態管理をシンプルに保つ」という原則を忘れずに、プロジェクトの段階で最適なパターンを選択していきましょう。
