react-state-management について
react-state-management は、状態管理の選択や設定を支援する React 開発者向けのリファレンススキルです。組み込み API、Context、Zustand、Jotai、TanStack Query、SWR を扱います。
主な機能
- 01用途別に状態管理方式を比較
- 02useState と useReducer を解説
- 03Context と Zustand のパターンを紹介
- 04Jotai と TanStack Query を扱う
- 05SWR、楽観的更新、正規化を解説
活用シーン
- 01React アプリの状態管理方式を選択
- 02共有状態ストアを構築
- 03サーバー状態のキャッシュを設定
- 04不要な再レンダリングを削減
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ josiahsiegel/ claude-plugin-marketplace --skill react-state-managementインストール後、Claude Code で/react-state-managementを入力して読み込まれたか確認します。
同梱ファイルreferences/jotai-and-tanstack-query.mdreferences/zustand-patterns.md
詳細
クイックリファレンス
| ライブラリ | 最適な用途 | インストール |
|---|---|---|
| Context | 小規模なアプリ、テーマ | 組み込み |
| Zustand | シンプルなグローバル状態 | npm i zustand |
| Jotai | アトミック/粒度の細かい状態 | npm i jotai |
| TanStack Query | サーバー状態/キャッシュ | npm i @tanstack/react-query |
| SWR | データ取得 | npm i swr |
| シナリオ | 推奨 |
|---|---|
| シンプルなローカル状態 | useState |
| 複雑なローカル状態 | useReducer |
| 共有状態(小規模なアプリ) | Context + useReducer |
| 共有状態(大規模なアプリ) | Zustand または Jotai |
| サーバー状態 | TanStack Query または SWR |
このスキルを使用する場面
状態管理に関する判断に使用します:
- 状態管理ソリューションの選択
- Zustand、Jotai、または Context ストアのセットアップ
- サーバー状態向けの TanStack Query の設定
- 楽観的更新の実装
- 複雑な状態構造の正規化
- 不要な再レンダリングの回避
React フックの基礎について:react-hooks-complete を参照してください
React の状態管理
組み込みの状態管理
useState によるコンポーネントの状態管理
'use client';
import { useState } from 'react';
function ShoppingCart() {
const [items, setItems] = useState<CartItem[]>([]);
const [isOpen, setIsOpen] = useState(false);
const addItem = (product: Product) => {
setItems((prev) => {
const existing = prev.find((item) => item.id === product.id);
if (existing) {
return prev.map((item) =>
item.id === product.id
? { ...item, quantity: item.quantity + 1 }
: item
);
}
return [...prev, { ...product, quantity: 1 }];
});
};
const total = items.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
return (
<div>
<button onClick={() => setIsOpen(!isOpen)}>
Cart ({items.length}) - ${total.toFixed(2)}
</button>
{isOpen && <CartDropdown items={items} />}
</div>
);
}
useReducer による複雑な状態管理
'use client';
import { useReducer, Dispatch, createContext, useContext } from 'react';
// Types
interface CartState {
items: CartItem[];
isLoading: boolean;
error: string | null;
}
type CartAction =
| { type: 'ADD_ITEM'; payload: Product }
| { type: 'REMOVE_ITEM'; payload: string }
| { type: 'UPDATE_QUANTITY'; payload: { id: string; quantity: number } }
| { type: 'CLEAR_CART' }
| { type: 'SET_LOADING'; payload: boolean }
| { type: 'SET_ERROR'; payload: string };
// Reducer
function cartReducer(state: CartState, action: CartAction): CartState {
switch (action.type) {
case 'ADD_ITEM': {
const existing = state.items.find(
(item) => item.id === action.payload.id
);
if (existing) {
return {
...state,
items: state.items.map((item) =>
item.id === action.payload.id
? { ...item, quantity: item.quantity + 1 }
: item
),
};
}
return {
...state,
items: [...state.items, { ...action.payload, quantity: 1 }],
};
}
case 'REMOVE_ITEM':
return {
...state,
items: state.items.filter((item) => item.id !== action.payload),
};
case 'UPDATE_QUANTITY':
return {
...state,
items: state.items.map((item) =>
item.id === action.payload.id
? { ...item, quantity: action.payload.quantity }
: item
),
};
case 'CLEAR_CART':
return { ...state, items: [] };
case 'SET_LOADING':
return { ...state, isLoading: action.payload };
case 'SET_ERROR':
return { ...state, error: action.payload };
default:
return state;
}
}
// Context
const CartContext = createContext<{
state: CartState;
dispatch: Dispatch<CartAction>;
} | null>(null);
// Provider
export function CartProvider({ children }: { children: React.ReactNode }) {
const [state, dispatch] = useReducer(cartReducer, {
items: [],
isLoading: false,
error: null,
});
return (
<CartContext.Provider value={{ state, dispatch }}>
{children}
</CartContext.Provider>
);
}
// Hook
export function useCart() {
const context = useContext(CartContext);
if (!context) {
throw new Error('useCart must be used within CartProvider');
}
return context;
}
Context API
Context の作成と使用
import { createContext, useContext, useState, ReactNode } from 'react';
// Theme context
interface Theme {
colors: { primary: string; secondary: string; background: string };
spacing: { sm: number; md: number; lg: number };
}
interface ThemeContextType {
theme: Theme;
setTheme: (theme: Theme) => void;
toggleDarkMode: () => void;
isDark: boolean;
}
const ThemeContext = createContext<ThemeContextType | null>(null);
const lightTheme: Theme = {
colors: { primary: '#3b82f6', secondary: '#8b5cf6', background: '#ffffff' },
spacing: { sm: 8, md: 16, lg: 24 },
};
const darkTheme: Theme = {
colors: { primary: '#60a5fa', secondary: '#a78bfa', background: '#1f2937' },
spacing: { sm: 8, md: 16, lg: 24 },
};
export function ThemeProvider({ children }: { children: ReactNode }) {
const [isDark, setIsDark] = useState(false);
const [theme, setTheme] = useState<Theme>(lightTheme);
const toggleDarkMode = () => {
setIsDark((prev) => !prev);
setTheme(isDark ? lightTheme : darkTheme);
};
return (
<ThemeContext.Provider value={{ theme, setTheme, toggleDarkMode, isDark }}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within ThemeProvider');
}
return context;
}
Context のパフォーマンス最適化
import { createContext, useContext, useMemo, useCallback, useState } from 'react';
// Split context to prevent unnecessary re-renders
const UserContext = createContext<User | null>(null);
const UserActionsContext = createContext<{
login: (email: string, password: string) => Promise<void>;
logout: () => void;
updateProfile: (data: Partial<User>) => Promise<void>;
} | null>(null);
export function UserProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const login = useCallback(async (email: string, password: string) => {
const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify({ email, password }),
});
const userData = await response.json();
setUser(userData);
}, []);
const logout = useCallback(() => {
setUser(null);
}, []);
const updateProfile = useCallback(async (data: Partial<User>) => {
const response = await fetch('/api/profile', {
method: 'PATCH',
body: JSON.stringify(data),
});
const updated = await response.json();
setUser(updated);
}, []);
// Memoize actions object
const actions = useMemo(
() => ({ login, logout, updateProfile }),
[login, logout, updateProfile]
);
return (
<UserContext.Provider value={user}>
<UserActionsContext.Provider value={actions}>
{children}
</UserActionsContext.Provider>
</UserContext.Provider>
);
}
// Separate hooks for data and actions
export function useUser() {
return useContext(UserContext);
}
export function useUserActions() {
const context = useContext(UserActionsContext);
if (!context) {
throw new Error('useUserActions must be used within UserProvider');
}
return context;
}
Zustand
Zustand ストアの基本
import { create } from 'zustand';
import { persist, devtools } from 'zustand/middleware';
interface CartStore {
items: CartItem[];
addItem: (product: Product) => void;
removeItem: (id: string) => void;
updateQuantity: (id: string, quantity: number) => void;
clearCart: () => void;
total: () => number;
}
export const useCartStore = create<CartStore>()(
devtools(
persist(
(set, get) => ({
items: [],
addItem: (product) =>
set((state) => {
const existing = state.items.find((item) => item.id === product.id);
if (existing) {
return {
items: state.items.map((item) =>
item.id === product.id
? { ...item, quantity: item.quantity + 1 }
: item
),
};
}
return { items: [...state.items, { ...product, quantity: 1 }] };
}),
removeItem: (id) =>
set((state) => ({
items: state.items.filter((item) => item.id !== id),
})),
updateQuantity: (id, quantity) =>
set((state) => ({
items: state.items.map((item) =>
item.id === id ? { ...item, quantity } : item
),
})),
clearCart: () => set({ items: [] }),
total: () =>
get().items.reduce(
(sum, item) => sum + item.price * item.quantity,
0
),
}),
{ name: 'cart-storage' }
)
)
);
// Usage in component
function CartButton() {
const items = useCartStore((state) => state.items);
const total = useCartStore((state) => state.total());
return (
<button>
Cart ({items.length}) - ${total.toFixed(2)}
</button>
);
}
Immer を使った Zustand
import { create } from 'zustand';
import { immer } from 'zustand/middleware/immer';
interface TodoStore {
todos: Todo[];
addTodo: (text: string) => void;
toggleTodo: (id: string) => void;
deleteTodo: (id: string) => void;
}
export const useTodoStore = create<TodoStore>()(
immer((set) => ({
todos: [],
addTodo: (text) =>
set((state) => {
state.todos.push({
id: crypto.randomUUID(),
text,
completed: false,
});
}),
toggleTodo: (id) =>
set((state) => {
const todo = state.todos.find((t) => t.id === id);
if (todo) {
todo.completed = !todo.completed;
}
}),
deleteTodo: (id) =>
set((state) => {
const index = state.todos.findIndex((t) => t.id === id);
if (index !== -1) {
state.todos.splice(index, 1);
}
}),
}))
);
Jotai と TanStack Query
Jotai のアトムベースの状態管理と TanStack Query によるサーバー状態管理の詳細なパターン(アトム、派生アトム、非同期アトム、クライアントのクエリ、ミューテーション、無効化、楽観的更新、キャッシュの調整)は、references/jotai-and-tanstack-query.md に記載されています。アトムの合成や本番環境向けのサーバー状態キャッシュを選択する際は、このリファレンスを読み込んでください。
SWR
SWR の基本的な使い方
import useSWR, { SWRConfig } from 'swr';
const fetcher = (url: string) => fetch(url).then((res) => res.json());
function App() {
return (
<SWRConfig
value={{
fetcher,
refreshInterval: 0,
revalidateOnFocus: true,
dedupingInterval: 2000,
}}
>
<Dashboard />
</SWRConfig>
);
}
function Dashboard() {
const { data, error, isLoading, mutate } = useSWR('/api/dashboard');
if (error) return <div>Failed to load</div>;
if (isLoading) return <div>Loading...</div>;
return (
<div>
<h1>Dashboard</h1>
<p>Total Users: {data.totalUsers}</p>
<button onClick={() => mutate()}>Refresh</button>
</div>
);
}
SWR のミューテーション
import useSWRMutation from 'swr/mutation';
async function createUser(url: string, { arg }: { arg: CreateUserInput }) {
const res = await fetch(url, {
method: 'POST',
body: JSON.stringify(arg),
});
return res.json();
}
function CreateUserForm() {
const { trigger, isMutating } = useSWRMutation('/api/users', createUser);
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
await trigger({
name: formData.get('name') as string,
email: formData.get('email') as string,
});
};
return (
<form onSubmit={handleSubmit}>
<input name="name" required />
<input name="email" type="email" required />
<button disabled={isMutating}>
{isMutating ? 'Creating...' : 'Create'}
</button>
</form>
);
}
ベストプラクティス
1. 適切なツールを選ぶ
| シナリオ | 推奨 |
|---|---|
| シンプルなローカル状態 | useState |
| 複雑なローカル状態 | useReducer |
| 共有状態(小規模なアプリ) | Context + useReducer |
| 共有状態(大規模なアプリ) | Zustand または Jotai |
| サーバー状態 | TanStack Query または SWR |
2. Props のバケツリレーを避ける
// Instead of passing props through many levels
<Parent user={user}>
<Child user={user}>
<GrandChild user={user} />
</Child>
</Parent>
// Use context or state management
<UserProvider>
<Parent>
<Child>
<GrandChild /> {/* Access user via useUser() */}
</Child>
</Parent>
</UserProvider>
3. 複雑な状態を正規化する
// Instead of nested objects
const badState = {
posts: [
{ id: 1, title: 'Post 1', author: { id: 1, name: 'Alice' } },
{ id: 2, title: 'Post 2', author: { id: 1, name: 'Alice' } },
],
};
// Use normalized structure
const goodState = {
posts: {
byId: { 1: { id: 1, title: 'Post 1', authorId: 1 } },
allIds: [1, 2],
},
authors: {
byId: { 1: { id: 1, name: 'Alice' } },
allIds: [1],
},
};
参考情報
詳細なパターンや高度なユースケースについては、以下を参照してください:
references/zustand-patterns.md- スライス、ミドルウェア、テストを含む Zustand の高度なパターン
コメント
Markdown 対応