はじめに#
前回の記事では、React Routerを使ったSPAのルーティングについて解説しました。本記事では、アプリケーション全体で状態を共有するための「グローバル状態管理」について詳しく解説します。
これまでの学習で、useStateによるローカル状態管理や「状態のリフトアップ」によるコンポーネント間のデータ共有を学びました。しかし、アプリケーションが大きくなると、深くネストしたコンポーネントへpropsを渡し続ける「prop drilling」という問題が発生します。この問題を解決するのがグローバル状態管理です。
本記事では、Reactに組み込まれているContext APIと、軽量で人気の高い状態管理ライブラリZustandを取り上げ、それぞれの特徴と使い分けを解説します。
本記事を読むことで、以下のことができるようになります。
- prop drillingの問題を理解し、解決策を選択できる
- Context APIによるグローバル状態管理の実装
- Zustandの導入とストア設計
- プロジェクト規模に応じた状態管理ライブラリの選択
- 実践的なテーマ切り替え・認証状態管理の実装
実行環境・前提条件#
必要な環境#
- Node.js 20.x以上
- Viteで作成したReactプロジェクト(TypeScript推奨)
- VS Code(推奨)
前提知識#
- 関数コンポーネントの基本
- useState・useEffectの使い方
- Propsによるデータの受け渡し
- 状態のリフトアップの概念
prop drillingの問題#
prop drillingとは#
「prop drilling」とは、深くネストしたコンポーネントにデータを渡すために、中間のコンポーネントがそのデータを使用しないにもかかわらずpropsとして受け渡し続ける状況を指します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
// prop drillingの例 - データを使わない中間コンポーネントもpropsを受け渡す
function App() {
const [user, setUser] = useState({ name: "田中太郎", role: "admin" });
return <Layout user={user} />;
}
function Layout({ user }) {
return (
<div>
<Header user={user} />
<Main user={user} />
</div>
);
}
function Header({ user }) {
return (
<header>
<Navigation user={user} />
</header>
);
}
function Navigation({ user }) {
return (
<nav>
<UserMenu user={user} />
</nav>
);
}
function UserMenu({ user }) {
// 実際にuserデータを使用するのはここだけ
return <span>ようこそ、{user.name}さん</span>;
}
|
prop drillingの問題点#
上記の例では、userデータを必要としているのは最下層のUserMenuコンポーネントだけですが、Layout、Header、Navigationという3つの中間コンポーネントがpropsを受け渡しています。
この構造には以下の問題があります。
- 保守性の低下: 中間コンポーネントがデータを使わないのにpropsを定義する必要がある
- リファクタリングの困難さ: propsの型や名前を変更する際、すべての中間コンポーネントを修正する必要がある
- 可読性の低下: どのコンポーネントが実際にデータを使用しているか把握しにくい
- 再利用性の低下: 中間コンポーネントが特定のpropsに依存し、他の場所で使いにくくなる
Context API - Reactの組み込みソリューション#
Context APIとは#
Context APIは、React 16.3で導入されたグローバル状態管理のための組み込み機能です。コンポーネントツリーのどこからでも、propsを経由せずに直接データにアクセスできます。
Context APIは以下の3つのステップで使用します。
- Contextの作成:
createContextでContextオブジェクトを作成
- Providerで値を提供: コンポーネントツリーをProviderでラップし、値を設定
- useContextで値を取得: 任意のコンポーネントから値にアクセス
基本的な使い方#
まず、テーマ切り替え機能を例にContext APIの基本を学びましょう。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
|
// src/contexts/ThemeContext.tsx
import { createContext, useContext, useState, ReactNode } from 'react';
// 1. 型定義
type Theme = 'light' | 'dark';
type ThemeContextType = {
theme: Theme;
toggleTheme: () => void;
};
// 2. Contextの作成(デフォルト値はnull)
const ThemeContext = createContext<ThemeContextType | null>(null);
// 3. カスタムフック - Contextを安全に使用するため
export function useTheme() {
const context = useContext(ThemeContext);
if (context === null) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
}
// 4. Providerコンポーネント
type ThemeProviderProps = {
children: ReactNode;
};
export function ThemeProvider({ children }: ThemeProviderProps) {
const [theme, setTheme] = useState<Theme>('light');
const toggleTheme = () => {
setTheme(prev => prev === 'light' ? 'dark' : 'light');
};
return (
<ThemeContext value={{ theme, toggleTheme }}>
{children}
</ThemeContext>
);
}
|
Providerでアプリをラップ#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
// src/App.tsx
import { ThemeProvider } from './contexts/ThemeContext';
import { Header } from './components/Header';
import { Main } from './components/Main';
function App() {
return (
<ThemeProvider>
<div className="app">
<Header />
<Main />
</div>
</ThemeProvider>
);
}
export default App;
|
任意のコンポーネントからContextを使用#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
// src/components/Header.tsx
import { useTheme } from '../contexts/ThemeContext';
export function Header() {
const { theme, toggleTheme } = useTheme();
return (
<header className={`header ${theme}`}>
<h1>My App</h1>
<button onClick={toggleTheme}>
{theme === 'light' ? 'ダークモードへ' : 'ライトモードへ'}
</button>
</header>
);
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
// src/components/Main.tsx
import { useTheme } from '../contexts/ThemeContext';
export function Main() {
const { theme } = useTheme();
return (
<main className={`main ${theme}`}>
<p>現在のテーマ: {theme}</p>
<DeepNestedComponent />
</main>
);
}
// 深くネストしたコンポーネントからも直接アクセス可能
function DeepNestedComponent() {
const { theme } = useTheme();
return (
<div className={`nested ${theme}`}>
<p>このコンポーネントもテーマにアクセスできます</p>
</div>
);
}
|
実践例: 認証状態の管理#
より実践的な例として、ユーザー認証状態を管理するContextを実装します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
|
// src/contexts/AuthContext.tsx
import { createContext, useContext, useState, ReactNode, useCallback } from 'react';
// 型定義
type User = {
id: string;
name: string;
email: string;
role: 'user' | 'admin';
};
type AuthContextType = {
user: User | null;
isAuthenticated: boolean;
login: (email: string, password: string) => Promise<void>;
logout: () => void;
isLoading: boolean;
};
// Contextの作成
const AuthContext = createContext<AuthContextType | null>(null);
// カスタムフック
export function useAuth() {
const context = useContext(AuthContext);
if (context === null) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
}
// Providerコンポーネント
type AuthProviderProps = {
children: ReactNode;
};
export function AuthProvider({ children }: AuthProviderProps) {
const [user, setUser] = useState<User | null>(null);
const [isLoading, setIsLoading] = useState(false);
const login = useCallback(async (email: string, password: string) => {
setIsLoading(true);
try {
// 実際のアプリケーションではAPIを呼び出す
const response = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
});
if (!response.ok) {
throw new Error('ログインに失敗しました');
}
const userData = await response.json();
setUser(userData);
} finally {
setIsLoading(false);
}
}, []);
const logout = useCallback(() => {
setUser(null);
}, []);
const value = {
user,
isAuthenticated: user !== null,
login,
logout,
isLoading,
};
return (
<AuthContext value={value}>
{children}
</AuthContext>
);
}
|
複数のProviderを組み合わせる#
複数のContextを使用する場合は、Providerをネストします。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
// src/App.tsx
import { ThemeProvider } from './contexts/ThemeContext';
import { AuthProvider } from './contexts/AuthContext';
import { AppContent } from './components/AppContent';
function App() {
return (
<AuthProvider>
<ThemeProvider>
<AppContent />
</ThemeProvider>
</AuthProvider>
);
}
export default App;
|
Context APIのパフォーマンス最適化#
Context APIには、値が変更されるとそのContextを使用しているすべてのコンポーネントが再レンダリングされるという特性があります。これを最適化するにはいくつかの方法があります。
Contextを分割する#
1
2
3
4
5
6
7
8
9
10
11
12
13
|
// 良くない例 - すべての値が1つのContextに
const AppContext = createContext({
user: null,
theme: 'light',
notifications: [],
settings: {},
});
// 良い例 - 関連する値ごとにContextを分割
const UserContext = createContext(null);
const ThemeContext = createContext('light');
const NotificationContext = createContext([]);
const SettingsContext = createContext({});
|
useMemoで値をメモ化#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
export function ThemeProvider({ children }: ThemeProviderProps) {
const [theme, setTheme] = useState<Theme>('light');
const toggleTheme = useCallback(() => {
setTheme(prev => prev === 'light' ? 'dark' : 'light');
}, []);
// 値をメモ化して不要な再レンダリングを防ぐ
const value = useMemo(() => ({
theme,
toggleTheme,
}), [theme, toggleTheme]);
return (
<ThemeContext value={value}>
{children}
</ThemeContext>
);
}
|
Context APIの適切なユースケース#
Context APIは以下のような場面で効果的です。
- テーマ切り替え: ライト/ダークモードの切り替え
- 認証状態: ログイン中のユーザー情報
- 言語設定: 国際化対応(i18n)
- アプリ設定: ユーザーの設定項目
- 更新頻度の低いデータ: 頻繁に変更されないグローバルな値
Zustand - シンプルで高速な状態管理#
Zustandとは#
Zustand(ドイツ語で「状態」)は、軽量でシンプルな状態管理ライブラリです。2025年12月現在、GitHubで56,000以上のスターを獲得し、多くのReactプロジェクトで採用されています。
Zustandの主な特徴は以下の通りです。
- シンプルなAPI: ボイラープレートが少なく、学習コストが低い
- Providerが不要: Context APIのようなProviderでラップする必要がない
- 高いパフォーマンス: セレクターによる細かな再レンダリング制御
- ミドルウェア対応: devtools、persist、immerなど豊富な拡張機能
- TypeScript対応: 型安全な状態管理
Zustandのインストール#
基本的な使い方#
Zustandでは、create関数を使ってストア(状態を管理するフック)を作成します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
// src/stores/counterStore.ts
import { create } from 'zustand';
// 型定義
type CounterState = {
count: number;
increment: () => void;
decrement: () => void;
reset: () => void;
};
// ストアの作成
export const useCounterStore = create<CounterState>((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
reset: () => set({ count: 0 }),
}));
|
コンポーネントからストアを使用#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
// src/components/Counter.tsx
import { useCounterStore } from '../stores/counterStore';
export function Counter() {
// 必要な状態だけを選択(セレクター)
const count = useCounterStore((state) => state.count);
const increment = useCounterStore((state) => state.increment);
const decrement = useCounterStore((state) => state.decrement);
const reset = useCounterStore((state) => state.reset);
return (
<div>
<h2>カウンター: {count}</h2>
<button onClick={increment}>+1</button>
<button onClick={decrement}>-1</button>
<button onClick={reset}>リセット</button>
</div>
);
}
|
セレクターによるパフォーマンス最適化#
Zustandの大きな利点は、セレクターを使って必要な状態だけを購読できることです。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
// 良い例 - 必要な値だけを購読
function DisplayCount() {
// countが変更されたときだけ再レンダリング
const count = useCounterStore((state) => state.count);
return <p>Count: {count}</p>;
}
function IncrementButton() {
// incrementは変更されないので再レンダリングされない
const increment = useCounterStore((state) => state.increment);
return <button onClick={increment}>+1</button>;
}
// 注意が必要な例 - すべての状態を購読
function Counter() {
// ストア内のどの値が変更されても再レンダリング
const state = useCounterStore();
return <p>Count: {state.count}</p>;
}
|
実践例: Todoリストの状態管理#
より実践的な例として、Todoリストを管理するストアを実装します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
|
// src/stores/todoStore.ts
import { create } from 'zustand';
// 型定義
type Todo = {
id: string;
text: string;
completed: boolean;
createdAt: Date;
};
type FilterType = 'all' | 'active' | 'completed';
type TodoState = {
todos: Todo[];
filter: FilterType;
// Actions
addTodo: (text: string) => void;
toggleTodo: (id: string) => void;
deleteTodo: (id: string) => void;
setFilter: (filter: FilterType) => void;
clearCompleted: () => void;
};
export const useTodoStore = create<TodoState>((set) => ({
todos: [],
filter: 'all',
addTodo: (text) => set((state) => ({
todos: [
...state.todos,
{
id: crypto.randomUUID(),
text,
completed: false,
createdAt: new Date(),
},
],
})),
toggleTodo: (id) => set((state) => ({
todos: state.todos.map((todo) =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
),
})),
deleteTodo: (id) => set((state) => ({
todos: state.todos.filter((todo) => todo.id !== id),
})),
setFilter: (filter) => set({ filter }),
clearCompleted: () => set((state) => ({
todos: state.todos.filter((todo) => !todo.completed),
})),
}));
// 派生状態を計算するセレクター
export const selectFilteredTodos = (state: TodoState) => {
switch (state.filter) {
case 'active':
return state.todos.filter((todo) => !todo.completed);
case 'completed':
return state.todos.filter((todo) => todo.completed);
default:
return state.todos;
}
};
export const selectTodoStats = (state: TodoState) => ({
total: state.todos.length,
active: state.todos.filter((t) => !t.completed).length,
completed: state.todos.filter((t) => t.completed).length,
});
|
Todoリストコンポーネントの実装#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
|
// src/components/TodoApp.tsx
import { useState } from 'react';
import { useTodoStore, selectFilteredTodos, selectTodoStats } from '../stores/todoStore';
export function TodoApp() {
return (
<div className="todo-app">
<h1>Todoリスト</h1>
<TodoInput />
<TodoFilter />
<TodoList />
<TodoStats />
</div>
);
}
function TodoInput() {
const [text, setText] = useState('');
const addTodo = useTodoStore((state) => state.addTodo);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (text.trim()) {
addTodo(text.trim());
setText('');
}
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="新しいTodoを入力..."
/>
<button type="submit">追加</button>
</form>
);
}
function TodoFilter() {
const filter = useTodoStore((state) => state.filter);
const setFilter = useTodoStore((state) => state.setFilter);
const clearCompleted = useTodoStore((state) => state.clearCompleted);
return (
<div className="todo-filter">
<button
className={filter === 'all' ? 'active' : ''}
onClick={() => setFilter('all')}
>
すべて
</button>
<button
className={filter === 'active' ? 'active' : ''}
onClick={() => setFilter('active')}
>
未完了
</button>
<button
className={filter === 'completed' ? 'active' : ''}
onClick={() => setFilter('completed')}
>
完了済み
</button>
<button onClick={clearCompleted}>完了を削除</button>
</div>
);
}
function TodoList() {
const todos = useTodoStore(selectFilteredTodos);
const toggleTodo = useTodoStore((state) => state.toggleTodo);
const deleteTodo = useTodoStore((state) => state.deleteTodo);
if (todos.length === 0) {
return <p>Todoがありません</p>;
}
return (
<ul className="todo-list">
{todos.map((todo) => (
<li key={todo.id} className={todo.completed ? 'completed' : ''}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
/>
<span>{todo.text}</span>
<button onClick={() => deleteTodo(todo.id)}>削除</button>
</li>
))}
</ul>
);
}
function TodoStats() {
const stats = useTodoStore(selectTodoStats);
return (
<div className="todo-stats">
<span>合計: {stats.total}</span>
<span>未完了: {stats.active}</span>
<span>完了: {stats.completed}</span>
</div>
);
}
|
非同期処理の実装#
Zustandでは、アクション内で非同期処理を自然に記述できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
|
// src/stores/userStore.ts
import { create } from 'zustand';
type User = {
id: string;
name: string;
email: string;
};
type UserState = {
users: User[];
isLoading: boolean;
error: string | null;
fetchUsers: () => Promise<void>;
addUser: (user: Omit<User, 'id'>) => Promise<void>;
};
export const useUserStore = create<UserState>((set, get) => ({
users: [],
isLoading: false,
error: null,
fetchUsers: async () => {
set({ isLoading: true, error: null });
try {
const response = await fetch('/api/users');
if (!response.ok) throw new Error('ユーザーの取得に失敗しました');
const users = await response.json();
set({ users, isLoading: false });
} catch (error) {
set({
error: error instanceof Error ? error.message : '不明なエラー',
isLoading: false
});
}
},
addUser: async (userData) => {
set({ isLoading: true, error: null });
try {
const response = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(userData),
});
if (!response.ok) throw new Error('ユーザーの追加に失敗しました');
const newUser = await response.json();
// 現在の状態を取得して更新
set((state) => ({
users: [...state.users, newUser],
isLoading: false
}));
} catch (error) {
set({
error: error instanceof Error ? error.message : '不明なエラー',
isLoading: false
});
}
},
}));
|
ミドルウェアの活用#
Zustandは豊富なミドルウェアを提供しています。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
import { create } from 'zustand';
import { devtools } from 'zustand/middleware';
export const useCounterStore = create<CounterState>()(
devtools(
(set) => ({
count: 0,
increment: () => set(
(state) => ({ count: state.count + 1 }),
undefined,
'counter/increment' // アクション名
),
decrement: () => set(
(state) => ({ count: state.count - 1 }),
undefined,
'counter/decrement'
),
}),
{ name: 'CounterStore' }
)
);
|
persist - ローカルストレージへの永続化#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
type SettingsState = {
theme: 'light' | 'dark';
language: string;
setTheme: (theme: 'light' | 'dark') => void;
setLanguage: (language: string) => void;
};
export const useSettingsStore = create<SettingsState>()(
persist(
(set) => ({
theme: 'light',
language: 'ja',
setTheme: (theme) => set({ theme }),
setLanguage: (language) => set({ language }),
}),
{
name: 'app-settings', // ストレージのキー名
storage: createJSONStorage(() => localStorage),
}
)
);
|
immer - 不変性を意識しない状態更新#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
import { create } from 'zustand';
import { immer } from 'zustand/middleware/immer';
type TodoState = {
todos: { id: string; text: string; completed: boolean }[];
toggleTodo: (id: string) => void;
};
export const useTodoStore = create<TodoState>()(
immer((set) => ({
todos: [],
toggleTodo: (id) => set((state) => {
// immerにより直接的な変更が可能
const todo = state.todos.find((t) => t.id === id);
if (todo) {
todo.completed = !todo.completed;
}
}),
}))
);
|
複数のミドルウェアを組み合わせる#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
import { create } from 'zustand';
import { devtools, persist, createJSONStorage } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer';
export const useTodoStore = create<TodoState>()(
devtools(
persist(
immer((set) => ({
// ... 状態とアクション
})),
{
name: 'todo-storage',
storage: createJSONStorage(() => localStorage),
}
),
{ name: 'TodoStore' }
)
);
|
Context API vs Zustand - 使い分けの指針#
比較表#
| 特徴 |
Context API |
Zustand |
| 追加パッケージ |
不要(React組み込み) |
必要(約1KB gzip) |
| Providerの必要性 |
必要 |
不要 |
| 学習コスト |
低い |
低い |
| ボイラープレート |
やや多い |
少ない |
| 再レンダリング最適化 |
手動で対応が必要 |
セレクターで自動最適化 |
| ミドルウェア |
なし |
豊富(devtools, persist等) |
| テスト容易性 |
Providerのモックが必要 |
直接ストアをテスト可能 |
| React外からのアクセス |
不可 |
可能 |
Context APIを選ぶべきケース#
以下のような状況では、Context APIが適しています。
- 依存関係を増やしたくない場合: 追加パッケージなしで利用可能
- 更新頻度が低いデータ: テーマ、言語設定、認証状態など
- シンプルな状態管理: 小規模なアプリケーションや一部の機能
- Reactエコシステムに閉じた開発: React外からの状態アクセスが不要
1
2
3
4
5
6
7
8
9
|
// Context APIが適している例
// テーマ設定(更新頻度: 低)
const ThemeContext = createContext<ThemeContextType | null>(null);
// 認証状態(更新頻度: 低〜中)
const AuthContext = createContext<AuthContextType | null>(null);
// アプリ設定(更新頻度: 低)
const SettingsContext = createContext<SettingsContextType | null>(null);
|
Zustandを選ぶべきケース#
以下のような状況では、Zustandが適しています。
- 頻繁に更新される状態: フォーム入力、リアルタイムデータなど
- 複雑な状態ロジック: 多くのアクションや派生状態がある場合
- パフォーマンスが重要: 大量のデータや頻繁な更新がある場合
- デバッグ体験の向上: Redux DevToolsでの状態追跡が必要
- 永続化が必要: ローカルストレージへの自動保存
- React外からのアクセス: イベントハンドラやユーティリティ関数からの状態操作
1
2
3
4
5
6
7
8
9
|
// Zustandが適している例
// Todoリスト(更新頻度: 高、CRUD操作が多い)
export const useTodoStore = create<TodoState>((set) => ({...}));
// ショッピングカート(更新頻度: 中〜高、永続化が必要)
export const useCartStore = create(persist(...));
// フォーム状態(更新頻度: 高)
export const useFormStore = create<FormState>((set) => ({...}));
|
両方を組み合わせる#
実際のアプリケーションでは、Context APIとZustandを適材適所で組み合わせることも有効です。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
// App.tsx
import { AuthProvider } from './contexts/AuthContext'; // Context API
import { ThemeProvider } from './contexts/ThemeContext'; // Context API
import { TodoApp } from './components/TodoApp'; // Zustandを使用
function App() {
return (
<AuthProvider>
<ThemeProvider>
<TodoApp />
</ThemeProvider>
</AuthProvider>
);
}
|
他の状態管理ライブラリとの比較#
Reduxは歴史のある状態管理ライブラリで、Redux Toolkitによりボイラープレートが削減されています。
- メリット: 大規模なエコシステム、豊富なミドルウェア、Redux DevTools
- デメリット: Zustandより設定が複雑、学習コストがやや高い
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
// Redux Toolkit
import { createSlice, configureStore } from '@reduxjs/toolkit';
const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: {
increment: (state) => { state.value += 1; },
decrement: (state) => { state.value -= 1; },
},
});
// Zustand(より簡潔)
import { create } from 'zustand';
const useCounterStore = create((set) => ({
value: 0,
increment: () => set((s) => ({ value: s.value + 1 })),
decrement: () => set((s) => ({ value: s.value - 1 })),
}));
|
Jotai#
Jotaiは「アトム」という単位で状態を管理する、Zustandと同じpmndrsチームによるライブラリです。
- メリット: 細かな粒度での状態管理、React Suspense対応
- デメリット: アトム間の依存関係が複雑になりやすい
1
2
3
4
5
6
7
8
9
|
// Jotai
import { atom, useAtom } from 'jotai';
const countAtom = atom(0);
function Counter() {
const [count, setCount] = useAtom(countAtom);
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}
|
選択の指針#
| ライブラリ |
推奨ケース |
| Context API |
小規模アプリ、更新頻度の低いデータ |
| Zustand |
中〜大規模アプリ、シンプルさとパフォーマンスのバランス |
| Redux Toolkit |
大規模エンタープライズアプリ、既存のRedux資産がある場合 |
| Jotai |
細かな状態分割が必要な場合、Suspense活用 |
まとめ#
本記事では、Reactにおけるグローバル状態管理について、Context APIとZustandを中心に解説しました。
- prop drillingはコンポーネント間のデータ受け渡しを複雑にする問題
- Context APIはReact組み込みのソリューションで、更新頻度の低いデータに適している
- Zustandは軽量でシンプルなAPI、高いパフォーマンス、豊富なミドルウェアが特徴
- プロジェクトの規模や要件に応じて適切なツールを選択することが重要
- 両者を組み合わせて使うことも有効なアプローチ
グローバル状態管理をマスターすることで、スケーラブルで保守しやすいReactアプリケーションを構築できるようになります。次の記事では、TypeScriptとReactを組み合わせた型安全なコンポーネント開発について解説します。
次に読む記事#
参考リンク#