🎛️ 状態管理とは?
📖 定義
**状態(State)**はアプリケーションのデータとUI状態を意味し、状態管理はこのような状態を効率的に保存、更新、共有する方法です。Reactで複数のコンポーネントが同じデータを使用する時、状態管理ライブラリを通じて複雑性を減らし、コードをきれいに維持することができます。
🎯 比喩で理解する
銀行口座の比喩
状態管理を銀行システムに例えると:
個人財布 (Component State)
├─ 本人のみ使用
├─ 簡単で速い
└─ 他の人と共有不可
家族共同口座 (Context API)
├─ 家族全員がアクセス
├─ 設定簡単
└─ 家族が多くなると複雑
銀行システム (Redux/Zustand)
├─ 中央集中式管理
├─ すべての取引記録
├─ 複雑だが体系的
└─ 監査(audit)可能
例:
ユーザーログイン情報
├─ ヘッダー: ユーザー名表示
├─ サイドバー: プロフィール写真
├─ 設定: メール情報
└─ フッター: ログアウトボタン
すべてのコンポーネントが同じユーザー情報が必要
→ 状態管理で一箇所で管理!
放送局の比喩
地方放送 (Local State)
- 一つのコンポーネント内のみ
- useStateを使用
例: モーダル開閉状態
中央放送 (Global State)
- アプリ全体からアクセス
- Redux/Zustandを使用
例: ログインユーザー情報
ニュース速報 (State Updates)
- 中央から発信
- すべての購読者に配信
- 自動UI更新
リモコン (Actions)
- チャンネル変更リクエスト
- ボリューム調整リクエスト
- 明確なインターフェース
図書館の比喩
机上メモ (Local State)
- 個人作業スペース
- 速くて簡単
- 他の人が見れない
図書館掲示板 (Context API)
- 同じフロアの利用者が共有
- 中規模
- 設定簡単
中央データベース (Redux)
- 図書館全体のシステム
- すべての本の位置を追跡
- 貸出記録管理
- 体系的だが複雑
リアルタイム通知 (Reactive State)
- 本が返却されると自動通知
- 購読者にのみ送信
- Recoil/Jotai方式
⚙️ 動作原理
1. Props Drilling問題
// ❌ Props Drilling (地獄の階段)
// App.js
function App() {
const [user, setUser] = useState({ name: '田中太郎', email: 'tanaka@example.com' });
return <Layout user={user} setUser={setUser} />;
}
// Layout.js
function Layout({ user, setUser }) {
return (
<div>
<Header user={user} setUser={setUser} />
<Sidebar user={user} />
<Main user={user} setUser={setUser} />
</div>
);
}
// Header.js
function Header({ user, setUser }) {
return <UserMenu user={user} setUser={setUser} />;
}
// UserMenu.js
function UserMenu({ user, setUser }) {
return <ProfileButton user={user} setUser={setUser} />;
}
// ProfileButton.js
function ProfileButton({ user, setUser }) {
// ようやく使用!
return (
<button onClick={() => setUser({ ...user, name: '佐藤花子' })}>
{user.name}
</button>
);
}
// 問題点:
// 1. userを4段階も伝達 (App → Layout → Header → UserMenu → ProfileButton)
// 2. 中間コンポ ーネントが不要にpropsを受け取る
// 3. props追加/修正時にすべてのコンポーネントを修正
// 4. コード複雑度増加
// 5. メンテナンス困難
2. 状態管理の解決策
// ✅ 状態管理ライブラリを使用
// store.js (中央ストア)
const store = {
user: { name: '田中太郎', email: 'tanaka@example.com' }
};
// App.js
function App() {
return (
<div>
<Header />
<Sidebar />
<Main />
</div>
);
// props伝達不要!
}
// ProfileButton.js
function ProfileButton() {
// 必要なコンポーネントのみ直接アクセス
const user = useStore(state => state.user);
const updateUser = useStore(state => state.updateUser);
return (
<button onClick={() => updateUser({ name: '佐藤花子' })}>
{user.name}
</button>
);
}
// 利点:
// 1. props伝達不要
// 2. 中間コンポーネントへの影響なし
// 3. コードが簡潔
// 4. メンテナンスしやすい
// 5. パフォーマンス最適化可能
3. 単方向データフロー
// 状態管理の基本原則
┌─────────────────────────────────────┐
│ Store (ストア) │
│ { count: 0, user: {...}, ... } │
└─────────────────────────────────────┘
↓ 購読(subscribe)
↓
┌─────────────────────────────────────┐
│ Component (コンポーネント) │
│ UIレンダリング: <div>{count}</div> │
└─────────────────────────────────────┘
↓ イベント (例: ボタンクリック)
↓
┌─────────────────────────────────────┐
│ Action (アクション) │
│ { type: 'INCREMENT' } │
└─────────────────────────────────────┘
↓ 伝達(dispatch)
↓
┌─────────────────────────────────────┐
│ Reducer (リデューサー) │
│ count = count + 1 │
└─────────────────────────────────────┘
↓ 状態更新
↓
再びStoreへ ↰
// 特徴:
// 1. 予測可能: 同じ入力 → 同じ出力
// 2. 追跡可能: すべての変更記録
// 3. デバッグしやすい: タイムトラベル可能
// 4. テストしやすい: 純粋関数
💡 実際の例
Context API (基本)
// 1. Context生成
import { createContext, useContext, useState } from 'react';
const UserContext = createContext();
// 2. Providerコンポーネント
function UserProvider({ children }) {
const [user, setUser] = useState({
name: '田中太郎',
email: 'tanaka@example.com',
isLoggedIn: false
});
const login = (email, password) => {
// API呼び出し
setUser({
name: '田中太郎',
email: email,
isLoggedIn: true
});
};
const logout = () => {
setUser({
name: '',
email: '',
isLoggedIn: false
});
};
return (
<UserContext.Provider value={{ user, login, logout }}>
{children}
</UserContext.Provider>
);
}
// 3. Custom Hook
function useUser() {
const context = useContext(UserContext);
if (!context) {
throw new Error('useUser must be used within UserProvider');
}
return context;
}
// 4. AppでProviderで囲む
function App() {
return (
<UserProvider>
<Header />
<Main />
<Footer />
</UserProvider>
);
}
// 5. 使用
function Header() {
const { user, logout } = useUser();
return (
<header>
<h1>こんにちは、{user.name}さん</h1>
{user.isLoggedIn && (
<button onClick={logout}>ログアウト</button>
)}
</header>
);
}
function LoginForm() {
const { login } = useUser();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
login(email, password);
};
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<button type="submit">ログイン</button>
</form>
);
}
// Context APIの長所と短所:
// 長所:
// - 追加ライブラリ不要
// - 簡単な設定
// - React内蔵機能
// 短所:
// - パフォーマンス最適化が困難
// - Provider入れ子地獄
// - DevToolsなし
Redux (複雑だが強力)
// 1. Store設定
// store.js
import { configureStore, createSlice } from '@reduxjs/toolkit';
// Slice生成 (状態 + リデューサー)
const userSlice = createSlice({
name: 'user',
initialState: {
name: '',
email: '',
isLoggedIn: false,
loading: false,
error: null
},
reducers: {
loginStart: (state) => {
state.loading = true;
state.error = null;
},
loginSuccess: (state, action) => {
state.name = action.payload.name;
state.email = action.payload.email;
state.isLoggedIn = true;
state.loading = false;
},
loginFailure: (state, action) => {
state.loading = false;
state.error = action.payload;
},
logout: (state) => {
state.name = '';
state.email = '';
state.isLoggedIn = false;
}
}
});
// Actions export
export const { loginStart, loginSuccess, loginFailure, logout } = userSlice.actions;
// Thunk (非同期アクション)
export const loginAsync = (email, password) => async (dispatch) => {
dispatch(loginStart());
try {
const response = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
});
const data = await response.json();
dispatch(loginSuccess(data));
} catch (error) {
dispatch(loginFailure(error.message));
}
};
// Store生成
const store = configureStore({
reducer: {
user: userSlice.reducer
}
});
export default store;
// 2. AppにProvider接続
// index.js
import { Provider } from 'react-redux';
import store from './store';
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
// 3. コンポーネントで使用
// LoginForm.js
import { useDispatch, useSelector } from 'react-redux';
import { loginAsync } from './store';
function LoginForm() {
const dispatch = useDispatch();
const { loading, error } = useSelector(state => state.user);
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
dispatch(loginAsync(email, password));
};
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
disabled={loading}
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
disabled={loading}
/>
<button type="submit" disabled={loading}>
{loading ? 'ログイン中...' : 'ログイン'}
</button>
{error && <p style={{ color: 'red' }}>{error}</p>}
</form>
);
}
// Header.js
import { useDispatch, useSelector } from 'react-redux';
import { logout } from './store';
function Header() {
const dispatch = useDispatch();
const { name, isLoggedIn } = useSelector(state => state.user);
return (
<header>
{isLoggedIn && (
<>
<span>こんにちは、{name}さん</span>
<button onClick={() => dispatch(logout())}>
ログアウト
</button>
</>
)}
</header>
);
}
// Reduxの長所と短所:
// 長所:
// - 強力なDevTools (タイムトラベルデバッグ)
// - ミドルウェア (ロギング、非同期処理)
// - 巨大なエコシステム
// - 予測可能な状態管理
// - 厳格な構造
// 短所:
// - ボイラープレ ートが多い
// - 学習曲線が高い
// - 簡単なアプリには過剰
Zustand (シンプルで現代的)
// 1. Store生成
// store.js
import create from 'zustand';
const useStore = create((set, get) => ({
// 状態
user: {
name: '',
email: '',
isLoggedIn: false
},
loading: false,
error: null,
// アクション
login: async (email, password) => {
set({ loading: true, error: null });
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: {
name: data.name,
email: data.email,
isLoggedIn: true
},
loading: false
});
} catch (error) {
set({ error: error.message, loading: false });
}
},
logout: () => {
set({
user: {
name: '',
email: '',
isLoggedIn: false
}
});
},
// 派生状態 (computed)
getFullName: () => {
const { user } = get();
return user.name || 'ゲスト';
}
}));
export default useStore;
// 2. コンポーネントで使用 (Provider不要!)
// LoginForm.js
import useStore from './store';
function LoginForm() {
const { login, loading, error } = useStore();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
login(email, password);
};
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<button disabled={loading}>
{loading ? 'ログイン中...' : 'ログイン'}
</button>
{error && <p>{error}</p>}
</form>
);
}
// Header.js
import useStore from './store';
function Header() {
// 必要な状態のみ選択 (パフォーマンス最適化)
const user = useStore(state => state.user);
const logout = useStore(state => state.logout);
return (
<header>
{user.isLoggedIn && (
<>
<span>こんにちは、{user.name}さん</span>
<button onClick={logout}>ログアウト</button>
</>
)}
</header>
);
}
// 3. ミドルウェア使用
import create from 'zustand';
import { persist } from 'zustand/middleware';
// localStorageに自動保存
const useStore = create(
persist(
(set) => ({
user: { name: '', email: '', isLoggedIn: false },
login: async (email, password) => {
// ...
}
}),
{
name: 'user-storage' // localStorage key
}
)
);
// Zustandの長所と短所:
// 長所:
// - 非常にシンプルなAPI
// - Provider不要
// - TypeScript完全サポート
// - 小さいバンドルサイズ (1KB)
// - Redux DevToolsサポート
// - ミドルウェアサポート
// 短所:
// - エコシステムが小さい
// - 複雑なアプリでは?
// - 公式ドキュメント不足
Recoil (React親和的)
// 1. Atoms (状態単位)
// atoms.js
import { atom, selector } from 'recoil';
// Atom: 状態の断片
export const userState = atom({
key: 'userState',
default: {
name: '',
email: '',
isLoggedIn: false
}
});
export const loadingState = atom({
key: 'loadingState',
default: false
});
// Selector: 派生状態
export const userNameState = selector({
key: 'userNameState',
get: ({ get }) => {
const user = get(userState);
return user.name || 'ゲスト';
}
});
// 非同期Selector
export const userDataQuery = selector({
key: 'userDataQuery',
get: async ({ get }) => {
const response = await fetch('/api/user');
return response.json();
}
});
// 2. AppにRecoilRoot追加
// App.js
import { RecoilRoot } from 'recoil';
function App() {
return (
<RecoilRoot>
<Header />
<Main />
<Footer />
</RecoilRoot>
);
}
// 3. コンポーネントで使用
// LoginForm.js
import { useRecoilState, useSetRecoilState } from 'recoil';
import { userState, loadingState } from './atoms';
function LoginForm() {
const [user, setUser] = useRecoilState(userState);
const setLoading = useSetRecoilState(loadingState);
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const handleLogin = async (e) => {
e.preventDefault();
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({
name: data.name,
email: data.email,
isLoggedIn: true
});
} catch (error) {
console.error(error);
} finally {
setLoading(false);
}
};
return (
<form onSubmit={handleLogin}>
{/* ... */}
</form>
);
}
// Header.js
import { useRecoilValue } from 'recoil';
import { userNameState } from './atoms';
function Header() {
const userName = useRecoilValue(userNameState);
return (
<header>
<span>こんにちは、{userName}さん</span>
</header>
);
}
// 4. 非同期データ
import { useRecoilValueLoadable } from 'recoil';
import { userDataQuery } from './atoms';
function UserProfile() {
const userLoadable = useRecoilValueLoadable(userDataQuery);
switch (userLoadable.state) {
case 'loading':
return <div>ロード中...</div>;
case 'hasError':
return <div>エラー: {userLoadable.contents.message}</div>;
case 'hasValue':
return <div>{userLoadable.contents.name}</div>;
}
}
// Recoilの長所と短所:
// 長所:
// - React Hooksスタイル
// - 非同期処理が簡単
// - 派生状態の自動計算
// - Concurrent Modeサポート
// - Facebook製作
// 短所:
// - まだ実験段階
// - 安定性への懸念
// - エコシステムが小さい
// - DevTools不足
比較例: ショッピングカート
// Context API バージョン
const CartContext = createContext();
function CartProvider({ children }) {
const [items, setItems] = useState([]);
const addItem = (item) => {
setItems([...items, item]);
};
const removeItem = (id) => {
setItems(items.filter(item => item.id !== id));
};
const total = items.reduce((sum, item) => sum + item.price, 0);
return (
<CartContext.Provider value={{ items, addItem, removeItem, total }}>
{children}
</CartContext.Provider>
);
}
// Redux バージョン
const cartSlice = createSlice({
name: 'cart',
initialState: { items: [] },
reducers: {
addItem: (state, action) => {
state.items.push(action.payload);
},
removeItem: (state, action) => {
state.items = state.items.filter(item => item.id !== action.payload);
}
}
});
// Zustand バージョン
const useCartStore = create((set) => ({
items: [],
addItem: (item) => set((state) => ({
items: [...state.items, item]
})),
removeItem: (id) => set((state) => ({
items: state.items.filter(item => item.id !== id)
})),
total: () => {
const { items } = get();
return items.reduce((sum, item) => sum + item.price, 0);
}
}));
// Recoil バージョン
const cartState = atom({
key: 'cartState',
default: []
});
const cartTotalSelector = selector({
key: 'cartTotalSelector',
get: ({ get }) => {
const cart = get(cartState);
return cart.reduce((sum, item) => sum + item.price, 0);
}
});
// 使用
function ShoppingCart() {
// Context
const { items, removeItem, total } = useContext(CartContext);
// Redux
const items = useSelector(state => state.cart.items);
const dispatch = useDispatch();
// Zustand
const { items, removeItem, total } = useCartStore();
// Recoil
const [items, setItems] = useRecoilState(cartState);
const total = useRecoilValue(cartTotalSelector);
return (
<div>
{items.map(item => (
<div key={item.id}>
{item.name} - {item.price}円
<button onClick={() => removeItem(item.id)}>削除</button>
</div>
))}
<p>合計: {total}円</p>
</div>
);
}
パフォーマンス最適化
// 1. Context API パフォーマンス問題
function BadContext() {
const [user, setUser] = useState({ name: '', age: 0 });
const [theme, setTheme] = useState('light');
// 問題: themeだけ変わってもuserを使用するコンポーネントすべて再レンダリング
return (
<AppContext.Provider value={{ user, setUser, theme, setTheme }}>
{children}
</AppContext.Provider>
);
}
// ✅ 解決: Context分離
function GoodContext() {
return (
<UserContext.Provider value={{ user, setUser }}>
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
</UserContext.Provider>
);
}
// 2. Redux セレクター最適化
// ❌ 悪い例
function BadComponent() {
const state = useSelector(state => state); // 全体状態!
return <div>{state.user.name}</div>;
}
// ✅ 良い例
function GoodComponent() {
const userName = useSelector(state => state.user.name); // 必要なものだけ
return <div>{userName}</div>;
}
// 3. Zustand セレクター最適化
// ❌ 悪い例
function BadComponent() {
const store = useStore(); // 全体ストア
return <div>{store.user.name}</div>;
}
// ✅ 良い例
function GoodComponent() {
const userName = useStore(state => state.user.name); // 必要なものだけ
return <div>{userName}</div>;
}
// 4. Recoil atomFamily (動的状態)
const userItemState = atomFamily({
key: 'UserItem',
default: id => fetch(`/api/users/${id}`).then(r => r.json())
});
function UserProfile({ userId }) {
const user = useRecoilValue(userItemState(userId));
return <div>{user.name}</div>;
}
// 5. メモ化
import { memo } from 'react';
const ExpensiveComponent = memo(function ExpensiveComponent({ value }) {
console.log('レンダリング:', value);
return <div>{value}</div>;
});
// valueが同じなら再レンダリングしない!
🤔 よくある質問
Q1. どの状態管理ライブラリを選択すべきですか?
A: プロジェクト規模と要求事項によって異なります:
// 選択ガイド
// Context API - シンプルなアプリ
const contextUseCases = {
適している場合: [
'小さなプロジェクト',
'状態共有が少ない',
'追加ライブラリを使いたくない',
'学習目的'
],
例: [
'テーマ (ダーク/ ライトモード)',
'言語設定',
'シンプルな認証状態',
'モーダル状態'
],
長所: '別途インストール不要',
短所: 'パフォーマンス最適化が困難'
};
// Redux - 大規模で複雑なアプリ
const reduxUseCases = {
適している場合: [
'大規模プロジェクト',
'複雑な状態ロジック',
'タイムトラベルデバッグが必要',
'厳格な構造を求める',
'チームプロジェクト'
],
例: [
'企業向けダッシュボード',
'複雑なeコマース',
'金融アプリケーショ ン',
'SaaSプラットフォーム'
],
長所: '強力なDevTools、エコシステム',
短所: '学習曲線、ボイラープレート'
};
// Zustand - 中規模、現代的
const zustandUseCases = {
適している場合: [
'中規模プロジェクト',
'シンプルなAPIを好む',
'速い開発',
'TypeScript使用'
],
例: [
'一般的なWebアプリ',
'MVP開発',
'スタートアッププロジェクト',
'プロトタイプ'
],
長所: '