状態管理パターン
このガイドでは、フロントエンドアプリケーションにおける様々な種類の状態の管理方法を説明します。各ツールをいつ使用するかを理解することで、古いデータ、不要な再レンダリング、複雑なデバッグなどの一般的な問題を防ぐことができます。
このガイドを使用するタイミング
以下が必要な場合にこのガイドを使用してください:
- APIレスポンスをキャッシュし、サーバーと同期を保つ
- コンポーネント間でUI状態(サイドバー、テーマ、モーダル)を共有する
- データ取得のローディング状態とエラー状態を処理する
- より良いユーザー体験のための楽観的更新を実装する
- SaaSアプリケーションでマルチテナントコンテキストを管理する
Choosing the Right Tool
The most common mistake is using one tool for all state. Different types of state have different requirements:
| カテゴリ | ツール | 例 | Why This Tool |
|---|---|---|---|
| サーバー状態 | React Query | APIデータ、キャッシュされたレスポンス | Handles caching, background refetch, stale data automatically |
| クライアント状態 | Zustand | UI状態、ユーザー設定 | Simple API, no boilerplate, performant selectors |
| フォーム状態 | React Hook Form | フォーム入力、バリデーション | Optimized for form performance, built-in validation |
| URL状態 | Next.js Router | クエリパラメータ、パスパラメータ | Shareable URLs, browser history integration |
Common Problems and Solutions
| Problem | Wrong Approach | Right Approach |
|---|---|---|
| Data becomes stale after mutation | Manually update local state | Use React Query cache invalidation |
| Component re-renders on unrelated state changes | Subscribe to entire Zustand store | Use selectors to subscribe to specific values |
| Loading state shown on every page visit | Always fetch fresh data | Configure staleTime to serve cached data |
| User sees old data after edit | Wait for refetch | Use optimistic updates |
サーバー状態のためのReact Query
Use Case: Data List with Filtering
Scenario: Display a paginated list of products that users can filter by status or search.
Problem: Without caching, every filter change triggers a network request, even for previously loaded data.
Solution: React Query caches responses by query key, so returning to a previous filter serves cached data instantly.
// src/hooks/useProducts.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { productApi } from '@/services/api/products';
// Query keys factory - ensures consistent cache keys
export const productKeys = {
all: ['products'] as const,
lists: () => [...productKeys.all, 'list'] as const,
list: (filters: ProductFilters) => [...productKeys.lists(), filters] as const,
details: () => [...productKeys.all, 'detail'] as const,
detail: (id: string) => [...productKeys.details(), id] as const,
};
// List query hook
export function useProducts(filters: ProductFilters = {}) {
return useQuery({
queryKey: productKeys.list(filters),
queryFn: () => productApi.list(filters),
});
}
// Detail query hook
export function useProduct(id: string) {
return useQuery({
queryKey: productKeys.detail(id),
queryFn: () => productApi.get(id),
enabled: !!id,
});
}