Zustand × TanStack Query × SWR:キャッシュ・再検証・型安全の実運用比較

React アプリケーションの状態管理とデータフェッチングにおいて、Zustand、TanStack Query、SWR は現在最も注目されているライブラリです。それぞれが異なるアプローチでアプリケーションの課題を解決しますが、実際のプロジェクトでどれを選ぶべきか迷われる方も多いでしょう。
本記事では、これら 3 つのライブラリについて、キャッシュ戦略、再検証メカニズム、型安全性の観点から詳細に比較検証します。実運用での性能評価も含めて、あなたのプロジェクトに最適な選択肢を見つけるための指針をお届けします。
ライブラリ概要と特徴
Zustand の特徴と適用場面
Zustand は、シンプルさと軽量性を重視した状態管理ライブラリです。Redux のような複雑な設定や boilerplate コードを排除し、関数型アプローチで直感的な状態管理を実現しています。
以下の図は、Zustand の基本的なアーキテクチャを示しています。
mermaidflowchart LR
component["React Component"] -->|useStore| store["Zustand Store"]
store -->|状態更新| component
store -->|永続化| storage["LocalStorage"]
storage -->|復元| store
Zustand の核となる特徴は以下の通りです。
軽量性とシンプルさ
typescriptimport { create } from 'zustand';
interface UserState {
user: User | null;
setUser: (user: User) => void;
clearUser: () => void;
}
const useUserStore = create<UserState>((set) => ({
user: null,
setUser: (user) => set({ user }),
clearUser: () => set({ user: null }),
}));
型安全性の確保
typescript// TypeScript との親和性が高く、型推論が自動的に働きます
const UserProfile = () => {
const { user, setUser } = useUserStore();
// user の型は User | null として推論される
return (
<div>
{user?.name} {/* 型安全なアクセス */}
</div>
);
};
適用場面
Zustand は以下のようなケースで特に威力を発揮します。
- シンプルなグローバル状態管理が必要な場合
- Redux の学習コストを避けたい小〜中規模プロジェクト
- TypeScript プロジェクトでの型安全な状態管理
- ライブラリサイズを抑えたいモバイル向けアプリケーション
TanStack Query の特徴と適用場面
TanStack Query(旧 React Query)は、サーバー状態の管理に特化したライブラリです。データフェッチング、キャッシュ、同期、更新処理を包括的に解決し、クライアント側でのサーバー状態管理のベストプラクティスを提供します。
以下の図は、TanStack Query のデータフロー全体を示しています。
mermaidflowchart TD
component["React Component"] -->|useQuery| query["Query Hook"]
query -->|fetch| api["API Server"]
api -->|response| cache["Query Cache"]
cache -->|cached data| component
cache -->|background refetch| api
query -->|invalidate| cache
cache -->|garbage collection| memory["Memory"]
TanStack Query の主要な特徴は以下の通りです。
強力なキャッシュシステム
typescriptimport { useQuery } from '@tanstack/react-query';
interface Product {
id: number;
name: string;
price: number;
}
const ProductList = () => {
const { data, isLoading, error } = useQuery({
queryKey: ['products'],
queryFn: () =>
fetch('/api/products').then((res) => res.json()),
staleTime: 5 * 60 * 1000, // 5分間フレッシュ状態を維持
cacheTime: 10 * 60 * 1000, // 10分間キャッシュを保持
});
if (isLoading) return <div>読み込み中...</div>;
if (error) return <div>エラーが発生しました</div>;
return (
<ul>
{data?.map((product: Product) => (
<li key={product.id}>{product.name}</li>
))}
</ul>
);
};
楽観的更新とミューテーション
typescriptimport {
useMutation,
useQueryClient,
} from '@tanstack/react-query';
const useUpdateProduct = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (product: Product) =>
fetch(`/api/products/${product.id}`, {
method: 'PUT',
body: JSON.stringify(product),
}),
onMutate: async (newProduct) => {
// 楽観的更新の実装
await queryClient.cancelQueries({
queryKey: ['products'],
});
const previousProducts = queryClient.getQueryData([
'products',
]);
queryClient.setQueryData(
['products'],
(old: Product[]) =>
old.map((p) =>
p.id === newProduct.id ? newProduct : p
)
);
return { previousProducts };
},
onError: (err, newProduct, context) => {
// エラー時のロールバック
queryClient.setQueryData(
['products'],
context?.previousProducts
);
},
onSettled: () => {
queryClient.invalidateQueries({
queryKey: ['products'],
});
},
});
};
適用場面
TanStack Query は以下のシナリオで最大の効果を発揮します。
- API を多用するデータドリブンなアプリケーション
- リアルタイム性が重要なダッシュボードやモニタリングツール
- 複雑なデータ関係性を持つ管理画面
- オフライン対応が必要なプログレッシブウェブアプリケーション
SWR の特徴と適用場面
SWR(Stale-While-Revalidate)は、データフェッチングの戦略名そのものを冠したライブラリです。Vercel チームによって開発され、Next.js との親和性が高く、シンプルながら効果的なデータフェッチングソリューションを提供します。
以下の図は、SWR の Stale-While-Revalidate 戦略を図解しています。
mermaidsequenceDiagram
participant C as Component
participant S as SWR
participant Cache
participant API
C->>S: データ要求
S->>Cache: キャッシュ確認
Cache-->>S: Stale データ返却
S-->>C: Stale データ表示(即座に)
S->>API: バックグラウンド再検証
API-->>S: 最新データ
S->>Cache: キャッシュ更新
S-->>C: 最新データで再レンダリング
SWR のコア機能は以下の通りです。
シンプルなデータフェッチング
typescriptimport useSWR from 'swr';
interface User {
id: number;
name: string;
email: string;
}
const fetcher = (url: string) =>
fetch(url).then((res) => res.json());
const UserProfile = ({ userId }: { userId: number }) => {
const { data, error, isLoading } = useSWR<User>(
`/api/users/${userId}`,
fetcher
);
if (error) return <div>読み込みに失敗しました</div>;
if (isLoading) return <div>読み込み中...</div>;
return (
<div>
<h1>{data?.name}</h1>
<p>{data?.email}</p>
</div>
);
};
自動再検証とフォーカス管理
typescriptimport useSWR from 'swr';
const Dashboard = () => {
const { data, mutate } = useSWR(
'/api/dashboard',
fetcher,
{
refreshInterval: 1000, // 1秒ごとにポーリング
revalidateOnFocus: true, // ウィンドウフォーカス時に再検証
revalidateOnReconnect: true, // ネットワーク再接続時に再検証
}
);
// 手動でデータを再取得
const handleRefresh = () => {
mutate();
};
return (
<div>
<button onClick={handleRefresh}>更新</button>
<pre>{JSON.stringify(data, null, 2)}</pre>
</div>
);
};
条件付きフェッチング
typescriptconst ConditionalFetch = ({
shouldFetch,
userId,
}: {
shouldFetch: boolean;
userId?: number;
}) => {
// shouldFetch が false または userId が undefined の場合、
// フェッチを実行しない
const { data } = useSWR(
shouldFetch && userId ? `/api/users/${userId}` : null,
fetcher
);
return <div>{data?.name || '未選択'}</div>;
};
適用場面
SWR は以下のようなケースで特に有効です。
- Next.js プロジェクトでのデータフェッチング
- シンプルなデータ取得ロジックが中心のアプリケーション
- リアルタイム更新が重要な情報表示画面
- 学習コストを抑えたい小規模チーム開発
キャッシュ戦略の比較
キャッシュの仕組みとライフサイクル
各ライブラリのキャッシュ戦略には大きな違いがあります。それぞれのアプローチを詳しく見ていきましょう。
Zustand のキャッシュアプローチ
Zustand は基本的にクライアント状態管理に特化しており、従来的な意味でのキャッシュ機能は提供していません。しかし、永続化ミドルウェアを使用することで、状態の永続保存が可能です。
typescriptimport { create } from 'zustand';
import { persist } from 'zustand/middleware';
interface AppState {
userSettings: UserSettings;
updateSettings: (settings: Partial<UserSettings>) => void;
}
const useAppStore = create<AppState>()(
persist(
(set) => ({
userSettings: { theme: 'light', language: 'ja' },
updateSettings: (settings) =>
set((state) => ({
userSettings: {
...state.userSettings,
...settings,
},
})),
}),
{
name: 'app-storage', // localStorage キー名
storage: {
getItem: (name) => {
const value = localStorage.getItem(name);
return value ? JSON.parse(value) : null;
},
setItem: (name, value) => {
localStorage.setItem(name, JSON.stringify(value));
},
removeItem: (name) => localStorage.removeItem(name),
},
}
)
);
TanStack Query の多層キャッシュシステム
TanStack Query は、最も洗練されたキャッシュシステムを提供しています。以下の図は、そのキャッシュライフサイクルを示しています。
mermaidstateDiagram-v2
[*] --> fresh: データ取得
fresh --> stale: staleTime 経過
stale --> fetching: バックグラウンド再取得
fetching --> fresh: 新しいデータ取得
stale --> inactive: コンポーネントアンマウント
inactive --> garbage: cacheTime 経過
garbage --> [*]: メモリから削除
typescriptimport {
QueryClient,
QueryClientProvider,
} from '@tanstack/react-query';
// グローバル設定でキャッシュ戦略を定義
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000, // 5分間は fresh 状態
cacheTime: 30 * 60 * 1000, // 30分間キャッシュ保持
retry: 3, // 失敗時は3回まで再試行
retryDelay: (attemptIndex) =>
Math.min(1000 * 2 ** attemptIndex, 30000),
},
},
});
// 特定のクエリごとに詳細なキャッシュ設定
const ProductDetail = ({
productId,
}: {
productId: number;
}) => {
const { data } = useQuery({
queryKey: ['product', productId],
queryFn: () => fetchProduct(productId),
staleTime: 10 * 60 * 1000, // 商品情報は10分間 fresh
cacheTime: 60 * 60 * 1000, // 1時間キャッシュ保持
select: (data) => ({
// データ変換とメモ化
...data,
formattedPrice: new Intl.NumberFormat('ja-JP', {
style: 'currency',
currency: 'JPY',
}).format(data.price),
}),
});
return <div>{data?.formattedPrice}</div>;
};
SWR のシンプルキャッシュモデル
SWR は Stale-While-Revalidate 戦略に基づいたシンプルなキャッシュを提供します。
typescriptimport { SWRConfig } from 'swr';
// アプリ全体の SWR 設定
const App = () => (
<SWRConfig
value={{
refreshInterval: 3000, // 3秒ごとにポーリング
dedupingInterval: 2000, // 2秒以内の重複リクエストを無視
focusThrottleInterval: 5000, // フォーカス時再検証の間隔制限
errorRetryCount: 3, // エラー時の再試行回数
errorRetryInterval: 5000, // 再試行間隔
cache: new Map(), // カスタムキャッシュプロバイダー
}}
>
<MyApp />
</SWRConfig>
);
// ページレベルでのキャッシュ制御
const ProductPage = () => {
const { data, mutate } = useSWR(
'/api/products',
fetcher,
{
revalidateOnMount: true, // マウント時に常に再検証
revalidateIfStale: true, // stale 状態でも再検証実行
revalidateOnFocus: false, // フォーカス時再検証を無効化
}
);
// 手動でキャッシュをクリア
const clearCache = () => {
mutate(undefined, { revalidate: false });
};
return (
<div>
<button onClick={clearCache}>キャッシュクリア</button>
{/* 商品一覧の表示 */}
</div>
);
};
パフォーマンスへの影響
レンダリング最適化の比較
各ライブラリがレンダリングパフォーマンスに与える影響を検証してみましょう。
typescript// Zustand: 選択的購読によるレンダリング最適化
const OptimizedZustandComponent = () => {
// 必要な状態のみを選択的に購読
const userName = useUserStore(
(state) => state.user?.name
);
const userEmail = useUserStore(
(state) => state.user?.email
);
// userSettings が変更されてもこのコンポーネントは再レンダリングされない
return (
<div>
{userName} ({userEmail})
</div>
);
};
// TanStack Query: 自動的なレンダリング最適化
const OptimizedQueryComponent = () => {
const { data } = useQuery({
queryKey: ['user-profile'],
queryFn: fetchUserProfile,
select: (data) => ({
// 必要なフィールドのみを選択
displayName: data.firstName + ' ' + data.lastName,
avatar: data.profileImage,
}),
});
// select で変換されたデータが変更された場合のみ再レンダリング
return <div>{data?.displayName}</div>;
};
// SWR: 条件付きレンダリング最適化
const OptimizedSWRComponent = ({
userId,
}: {
userId?: number;
}) => {
const { data } = useSWR(
userId ? `/api/users/${userId}` : null, // 条件付きフェッチング
fetcher,
{
compare: (a, b) => {
// カスタム比較関数でレンダリングを制御
return a?.lastModified === b?.lastModified;
},
}
);
return <div>{data?.name}</div>;
};
メモリ使用量と効率性
メモリ効率性の比較表
ライブラリ | バンドルサイズ | メモリ使用量 | ガベージコレクション |
---|---|---|---|
Zustand | 2.9KB (gzipped) | 軽量(状態のみ) | 手動管理が必要 |
TanStack Query | 39KB (gzipped) | 中程度(自動管理) | 自動的なクリーンアップ |
SWR | 4.2KB (gzipped) | 軽量(シンプル設計) | タイムアウトベース |
メモリリーク対策の実装例
typescript// Zustand: 手動でのクリーンアップ
const useAutoCleanup = () => {
const store = useUserStore();
useEffect(() => {
return () => {
// コンポーネントアンマウント時にストアをクリア
store.clearUser();
};
}, []);
};
// TanStack Query: 自動的なガベージコレクション
const QueryWithGarbageCollection = () => {
const { data } = useQuery({
queryKey: ['large-dataset'],
queryFn: fetchLargeDataset,
cacheTime: 5 * 60 * 1000, // 5分後に自動削除
onSuccess: (data) => {
// 成功時のメモリ最適化
if (data.length > 1000) {
console.warn(
'大量データを取得しました。キャッシュ時間を短縮します。'
);
}
},
});
return <DataVisualization data={data} />;
};
// SWR: カスタムキャッシュプロバイダーでメモリ制御
const memoryEfficientCache = new Map();
// 最大キャッシュサイズを制限
const MAX_CACHE_SIZE = 100;
const customCache = {
get: (key: string) => memoryEfficientCache.get(key),
set: (key: string, value: any) => {
if (memoryEfficientCache.size >= MAX_CACHE_SIZE) {
// LRU で最も古いエントリを削除
const firstKey = memoryEfficientCache
.keys()
.next().value;
memoryEfficientCache.delete(firstKey);
}
memoryEfficientCache.set(key, value);
},
delete: (key: string) => memoryEfficientCache.delete(key),
};
再検証メカニズムの深掘り
リアルタイム更新の実装
WebSocket を使用したリアルタイム更新
各ライブラリでリアルタイム更新を実装する方法を比較してみましょう。
typescript// Zustand: WebSocket イベントリスナーで状態更新
interface RealtimeState {
notifications: Notification[];
onlineUsers: User[];
addNotification: (notification: Notification) => void;
updateOnlineUsers: (users: User[]) => void;
}
const useRealtimeStore = create<RealtimeState>((set) => {
// WebSocket 接続の初期化
const ws = new WebSocket(
'wss://api.example.com/realtime'
);
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
switch (data.type) {
case 'NOTIFICATION':
set((state) => ({
notifications: [
...state.notifications,
data.payload,
],
}));
break;
case 'USERS_UPDATE':
set({ onlineUsers: data.payload });
break;
}
};
return {
notifications: [],
onlineUsers: [],
addNotification: (notification) =>
set((state) => ({
notifications: [
...state.notifications,
notification,
],
})),
updateOnlineUsers: (users) =>
set({ onlineUsers: users }),
};
});
typescript// TanStack Query: invalidation によるリアルタイム同期
const useRealtimeInvalidation = () => {
const queryClient = useQueryClient();
useEffect(() => {
const ws = new WebSocket(
'wss://api.example.com/realtime'
);
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
switch (data.type) {
case 'USER_UPDATED':
// 特定ユーザーのクエリを無効化
queryClient.invalidateQueries({
queryKey: ['user', data.userId],
});
break;
case 'GLOBAL_UPDATE':
// 関連するすべてのクエリを無効化
queryClient.invalidateQueries({
predicate: (query) =>
query.queryKey[0] === 'users' ||
query.queryKey[0] === 'notifications',
});
break;
}
};
return () => ws.close();
}, [queryClient]);
};
// 楽観的更新を含むリアルタイム対応
const useOptimisticMessage = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (message: NewMessage) => {
return api.sendMessage(message);
},
onMutate: async (newMessage) => {
// 既存のクエリをキャンセル
await queryClient.cancelQueries({
queryKey: ['messages'],
});
const previousMessages = queryClient.getQueryData([
'messages',
]);
// 楽観的更新
queryClient.setQueryData(
['messages'],
(old: Message[]) => [
...old,
{
...newMessage,
id: 'temp-' + Date.now(),
status: 'sending',
},
]
);
return { previousMessages };
},
onSuccess: (data, newMessage) => {
// WebSocket で受信した実際のデータに更新
queryClient.setQueryData(
['messages'],
(old: Message[]) =>
old.map((msg) =>
msg.id.startsWith('temp-') &&
msg.content === newMessage.content
? data
: msg
)
);
},
onError: (err, newMessage, context) => {
// エラー時のロールバック
queryClient.setQueryData(
['messages'],
context?.previousMessages
);
},
});
};
typescript// SWR: mutate を使用したリアルタイム更新
const useRealtimeSWR = () => {
const { data: messages, mutate } = useSWR(
'/api/messages',
fetcher
);
useEffect(() => {
const ws = new WebSocket(
'wss://api.example.com/messages'
);
ws.onmessage = (event) => {
const newMessage = JSON.parse(event.data);
// 楽観的更新(サーバーからの再取得なし)
mutate(
(currentMessages: Message[]) => [
...currentMessages,
newMessage,
],
false
);
};
return () => ws.close();
}, [mutate]);
return { messages };
};
// 条件付きリアルタイム更新
const ConditionalRealtimeUpdate = ({
isActive,
}: {
isActive: boolean;
}) => {
const { data, mutate } = useSWR(
isActive ? '/api/active-data' : null,
fetcher,
{
refreshInterval: isActive ? 1000 : 0, // アクティブ時のみポーリング
revalidateOnFocus: isActive,
}
);
// 手動でのリアルタイム更新制御
const toggleRealtime = useCallback(() => {
if (isActive) {
mutate(); // 即座に更新
}
}, [isActive, mutate]);
return (
<div>
<button onClick={toggleRealtime}>手動更新</button>
<pre>{JSON.stringify(data, null, 2)}</pre>
</div>
);
};
バックグラウンド再検証
各ライブラリのバックグラウンド処理比較
mermaidsequenceDiagram
participant UI as User Interface
participant Z as Zustand
participant TQ as TanStack Query
participant S as SWR
participant API as API Server
Note over UI,API: ユーザーがページを離れた場合
UI->>Z: フォーカス離脱
Note over Z: バックグラウンド処理なし
UI->>TQ: フォーカス離脱
TQ->>API: バックグラウンド再検証継続
API-->>TQ: 最新データ
TQ->>TQ: キャッシュ更新
UI->>S: フォーカス離脱
S->>API: ポーリング継続(設定による)
API-->>S: 最新データ
S->>S: キャッシュ更新
Note over UI,API: ユーザーがページに戻った場合
UI->>Z: フォーカス復帰
Note over Z: 手動更新が必要
UI->>TQ: フォーカス復帰
TQ-->>UI: 最新キャッシュデータ表示
UI->>S: フォーカス復帰
S->>API: フォーカス時再検証
API-->>S: 確認・更新
S-->>UI: 最新データ表示
バックグラウンド同期の実装例
typescript// TanStack Query: 高度なバックグラウンド同期
const BackgroundSyncComponent = () => {
const { data, isLoading } = useQuery({
queryKey: ['dashboard-data'],
queryFn: fetchDashboardData,
refetchInterval: (data, query) => {
// 動的な更新間隔の設定
if (query.state.error) {
return 10000; // エラー時は10秒間隔
}
if (data?.priority === 'high') {
return 2000; // 高優先度データは2秒間隔
}
return 5000; // 通常は5秒間隔
},
refetchIntervalInBackground: true, // バックグラウンドでも継続
staleTime: 0, // 常に stale として扱い、積極的に更新
});
return <DashboardView data={data} loading={isLoading} />;
};
// ネットワーク状態を考慮したバックグラウンド同期
const NetworkAwareSync = () => {
const { data } = useQuery({
queryKey: ['network-sensitive-data'],
queryFn: fetchSensitiveData,
networkMode: 'offlineFirst', // オフライン時はキャッシュを優先
retry: (failureCount, error) => {
// ネットワークエラーの場合のみリトライ
return (
error.message.includes('network') &&
failureCount < 3
);
},
retryDelay: (attemptIndex) => {
// 指数バックオフ + ジッター
const baseDelay = Math.min(
1000 * 2 ** attemptIndex,
30000
);
const jitter = Math.random() * 0.1 * baseDelay;
return baseDelay + jitter;
},
});
return <NetworkSensitiveComponent data={data} />;
};
typescript// SWR: フォーカスベースの再検証制御
const FocusAwareComponent = () => {
const { data, mutate } = useSWR(
'/api/focus-sensitive',
fetcher,
{
revalidateOnFocus: true,
focusThrottleInterval: 5000, // 5秒以内の連続フォーカスを無視
dedupingInterval: 2000, // 2秒以内の重複リクエストを統合
}
);
// ページ可視性の変化を監視
useEffect(() => {
const handleVisibilityChange = () => {
if (document.visibilityState === 'visible') {
// ページが見えるようになったら手動で再検証
mutate();
}
};
document.addEventListener(
'visibilitychange',
handleVisibilityChange
);
return () => {
document.removeEventListener(
'visibilitychange',
handleVisibilityChange
);
};
}, [mutate]);
return <div>{data?.content}</div>;
};
// バッテリー状況を考慮した省電力モード
const BatteryAwarePolling = () => {
const [isLowBattery, setIsLowBattery] = useState(false);
useEffect(() => {
if ('getBattery' in navigator) {
navigator.getBattery().then((battery: any) => {
const updateBatteryInfo = () => {
setIsLowBattery(
battery.level < 0.2 && !battery.charging
);
};
battery.addEventListener(
'levelchange',
updateBatteryInfo
);
battery.addEventListener(
'chargingchange',
updateBatteryInfo
);
updateBatteryInfo();
});
}
}, []);
const { data } = useSWR('/api/battery-aware', fetcher, {
refreshInterval: isLowBattery ? 30000 : 5000, // 低バッテリー時は更新頻度を下げる
revalidateOnFocus: !isLowBattery, // 低バッテリー時はフォーカス再検証を無効化
});
return (
<div>
{isLowBattery && <div>省電力モードで動作中</div>}
<pre>{JSON.stringify(data, null, 2)}</pre>
</div>
);
};
エラーハンドリングと再試行
包括的なエラーハンドリング戦略
typescript// TanStack Query: 詳細なエラーハンドリング
const RobustErrorHandling = () => {
const { data, error, isError, failureCount } = useQuery({
queryKey: ['robust-data'],
queryFn: async () => {
const response = await fetch('/api/data');
if (!response.ok) {
// HTTP エラーの詳細を含む例外を投げる
throw new Error(
`HTTP ${response.status}: ${response.statusText}`
);
}
return response.json();
},
retry: (failureCount, error) => {
// 4xx エラーは再試行しない
if (error.message.includes('HTTP 4')) {
return false;
}
// 最大3回まで再試行
return failureCount < 3;
},
retryDelay: (attemptIndex) => {
// 指数バックオフ戦略
return Math.min(1000 * 2 ** attemptIndex, 30000);
},
onError: (error) => {
// エラーログとユーザー通知
console.error('データ取得エラー:', error);
// エラー種別に応じた処理
if (error.message.includes('HTTP 401')) {
// 認証エラーの場合、ログイン画面にリダイレクト
window.location.href = '/login';
} else if (error.message.includes('HTTP 503')) {
// サービス利用不可の場合、メンテナンス画面を表示
showMaintenanceModal();
}
},
});
if (isError) {
return (
<ErrorBoundary
error={error}
retryCount={failureCount}
>
<div>
エラーが発生しました: {error.message}
{failureCount > 0 && (
<div>再試行回数: {failureCount}</div>
)}
</div>
</ErrorBoundary>
);
}
return <DataDisplay data={data} />;
};
typescript// SWR: エラー時のフォールバック戦略
const SWRErrorHandling = () => {
const { data, error, isValidating, mutate } = useSWR(
'/api/unreliable-endpoint',
fetcher,
{
errorRetryCount: 3,
errorRetryInterval: 5000,
onError: (error, key) => {
// エラー発生時のログ記録
console.error(`SWR Error for ${key}:`, error);
// エラー通知サービスに送信
errorReportingService.captureException(error, {
context: { swrKey: key },
});
},
onErrorRetry: (
error,
key,
config,
revalidate,
{ retryCount }
) => {
// ネットワークエラーのみ再試行
if (error.status === 404) return;
// 最大5回まで再試行
if (retryCount >= 5) return;
// 再試行間隔をだんだん長くする
setTimeout(
() => revalidate({ retryCount }),
5000 * retryCount
);
},
fallbackData: [], // エラー時のフォールバックデータ
}
);
// エラー状態での UI 制御
if (error && !data) {
return (
<div>
<p>データの読み込みに失敗しました</p>
<button
onClick={() => mutate()}
disabled={isValidating}
>
{isValidating ? '再試行中...' : '再試行'}
</button>
</div>
);
}
// 部分的なエラー状態(キャッシュデータあり)
if (error && data) {
return (
<div>
<div
style={{
backgroundColor: '#fff3cd',
padding: '10px',
}}
>
最新データの取得に失敗しました。キャッシュデータを表示しています。
</div>
<DataList data={data} />
</div>
);
}
return <DataList data={data || []} />;
};
typescript// Zustand: カスタムエラーハンドリング
interface ErrorState {
errors: Record<string, Error>;
addError: (key: string, error: Error) => void;
clearError: (key: string) => void;
hasError: (key: string) => boolean;
}
const useErrorStore = create<ErrorState>((set, get) => ({
errors: {},
addError: (key, error) =>
set((state) => ({
errors: { ...state.errors, [key]: error },
})),
clearError: (key) =>
set((state) => {
const { [key]: removed, ...rest } = state.errors;
return { errors: rest };
}),
hasError: (key) => key in get().errors,
}));
// エラーハンドリング付きのデータフェッチング
const useDataWithErrorHandling = (endpoint: string) => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const { addError, clearError, hasError } =
useErrorStore();
const fetchData = useCallback(async () => {
setLoading(true);
clearError(endpoint);
try {
const response = await fetch(endpoint);
if (!response.ok) {
throw new Error(
`HTTP ${response.status}: ${response.statusText}`
);
}
const result = await response.json();
setData(result);
} catch (error) {
addError(endpoint, error as Error);
} finally {
setLoading(false);
}
}, [endpoint, addError, clearError]);
useEffect(() => {
fetchData();
}, [fetchData]);
return {
data,
loading,
error: hasError(endpoint),
refetch: fetchData,
};
};
型安全性の実装と比較
TypeScript 統合の品質
各ライブラリの TypeScript サポートには大きな差があります。実際のコード例を通して詳細に比較してみましょう。
Zustand の型安全性
typescript// 型定義の明示的な指定
interface UserState {
user: User | null
preferences: UserPreferences
setUser: (user: User) => void
updatePreferences: (prefs: Partial<UserPreferences>) => void
clearUser: () => void
}
// 完全に型安全なストア定義
const useUserStore = create<UserState>((set, get) => ({
user: null,
preferences: {
theme: 'light',
notifications: true,
language: 'ja',
},
setUser: (user: User) => set({ user }),
updatePreferences: (prefs: Partial<UserPreferences>) =>
set((state) => ({
preferences: { ...state.preferences, ...prefs }
})),
clearUser: () => set({ user: null }),
}))
// 選択的購読での型安全性
const UserProfile = () => {
// userName は string | undefined として型推論される
const userName = useUserStore((state) => state.user?.name)
// 複雑な選択でも型安全性が保たれる
const userInfo = useUserStore((state) => ({
hasUser: state.user !== null,
displayName: state.user?.name || 'ゲスト',
avatarUrl: state.user?.avatar || '/default-avatar.png',
}))
return (
<div>
<h1>{userInfo.displayName}</h1>
<img src={userInfo.avatarUrl} alt="ユーザーアバター" />
</div>
)
}
// ジェネリクスを使用した再利用可能なストア
interface AsyncState<T> {
data: T | null
loading: boolean
error: string | null
}
interface AsyncActions<T> {
setData: (data: T) => void
setLoading: (loading: boolean) => void
setError: (error: string | null) => void
reset: () => void
}
const createAsyncStore = <T>() =>
create<AsyncState<T> & AsyncActions<T>>((set) => ({
data: null,
loading: false,
error: null,
setData: (data: T) => set({ data, error: null }),
setLoading: (loading: boolean) => set({ loading }),
setError: (error: string | null) => set({ error, loading: false }),
reset: () => set({ data: null, loading: false, error: null }),
}))
// 型安全な専用ストアの作成
const useProductStore = createAsyncStore<Product[]>()
const useUserProfileStore = createAsyncStore<UserProfile>()
TanStack Query の高度な型推論
typescript// API レスポンスの型定義
interface ApiResponse<T> {
data: T
message: string
status: 'success' | 'error'
}
interface Product {
id: number
name: string
price: number
category: string
createdAt: string
}
// 型安全なクエリ関数の定義
const fetchProducts = async (): Promise<ApiResponse<Product[]>> => {
const response = await fetch('/api/products')
if (!response.ok) {
throw new Error('Failed to fetch products')
}
return response.json()
}
// 自動型推論を活用したクエリ
const ProductList = () => {
const {
data, // ApiResponse<Product[]> | undefined として推論
isLoading,
error // Error | null として推論
} = useQuery({
queryKey: ['products'],
queryFn: fetchProducts,
select: (response) => ({
// select の戻り値も自動で型推論される
products: response.data,
totalCount: response.data.length,
categories: [...new Set(response.data.map(p => p.category))],
}),
})
if (error) {
// error は Error 型として認識される
return <div>エラー: {error.message}</div>
}
if (isLoading) {
return <div>読み込み中...</div>
}
// data は select の戻り値型として推論される
return (
<div>
<h2>商品一覧 ({data?.totalCount}件)</h2>
<div>カテゴリ: {data?.categories.join(', ')}</div>
<ul>
{data?.products.map((product) => (
<li key={product.id}>
{product.name} - ¥{product.price.toLocaleString()}
</li>
))}
</ul>
</div>
)
}
// ジェネリクスを使用したカスタムフック
const useTypedQuery = <T>(
queryKey: readonly unknown[],
queryFn: () => Promise<ApiResponse<T>>,
options?: Omit<UseQueryOptions<ApiResponse<T>>, 'queryKey' | 'queryFn'>
) => {
return useQuery({
queryKey,
queryFn,
select: (response) => response.data, // T 型として推論される
...options,
})
}
// 使用例
const UserDetail = ({ userId }: { userId: number }) => {
const { data: user } = useTypedQuery(
['user', userId],
() => fetchUser(userId) // User 型として推論される
)
return <div>{user?.name}</div>
}
ミューテーションでの型安全性
typescript// ミューテーション用の型定義
interface CreateProductRequest {
name: string;
price: number;
categoryId: number;
}
interface UpdateProductRequest
extends Partial<CreateProductRequest> {
id: number;
}
// 型安全なミューテーション
const useCreateProduct = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (
product: CreateProductRequest
): Promise<Product> => {
const response = await fetch('/api/products', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(product),
});
if (!response.ok) {
throw new Error('Failed to create product');
}
return response.json();
},
onSuccess: (newProduct: Product) => {
// 既存のクエリキャッシュを更新
queryClient.setQueryData<ApiResponse<Product[]>>(
['products'],
(oldData) => {
if (!oldData) return oldData;
return {
...oldData,
data: [...oldData.data, newProduct],
};
}
);
},
});
};
// 楽観的更新での型安全性
const useOptimisticUpdateProduct = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (
product: UpdateProductRequest
): Promise<Product> => {
const response = await fetch(
`/api/products/${product.id}`,
{
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(product),
}
);
return response.json();
},
onMutate: async (
updatedProduct: UpdateProductRequest
) => {
await queryClient.cancelQueries({
queryKey: ['products'],
});
const previousProducts = queryClient.getQueryData<
ApiResponse<Product[]>
>(['products']);
// 型安全な楽観的更新
queryClient.setQueryData<ApiResponse<Product[]>>(
['products'],
(oldData) => {
if (!oldData) return oldData;
return {
...oldData,
data: oldData.data.map((product) =>
product.id === updatedProduct.id
? { ...product, ...updatedProduct }
: product
),
};
}
);
return { previousProducts }; // 型推論される
},
onError: (err, updatedProduct, context) => {
// context の型も自動推論される
if (context?.previousProducts) {
queryClient.setQueryData(
['products'],
context.previousProducts
);
}
},
});
};
SWR の型安全な実装
typescript// 型安全な fetcher 関数
const typedFetcher = <T>(url: string): Promise<T> =>
fetch(url).then((res) => {
if (!res.ok) {
throw new Error(`HTTP ${res.status}: ${res.statusText}`)
}
return res.json()
})
// ジェネリクスを活用したカスタムフック
const useTypedSWR = <T>(
key: string | (() => string | null),
options?: SWRConfiguration<T>
) => {
return useSWR<T, Error>(key, typedFetcher<T>, options)
}
// 使用例
interface UserProfile {
id: number
name: string
email: string
profile: {
bio: string
location: string
}
}
const UserProfileComponent = ({ userId }: { userId: number }) => {
const { data, error, mutate } = useTypedSWR<UserProfile>(
`/api/users/${userId}`
)
if (error) {
// error は Error 型として推論される
return <div>エラー: {error.message}</div>
}
if (!data) {
return <div>読み込み中...</div>
}
// data は UserProfile 型として推論される
return (
<div>
<h1>{data.name}</h1>
<p>{data.email}</p>
<p>{data.profile.bio}</p>
<p>場所: {data.profile.location}</p>
<button onClick={() => mutate()}>更新</button>
</div>
)
}
// 条件付きフェッチングでの型安全性
const ConditionalUserData = ({ shouldFetch, userId }: {
shouldFetch: boolean
userId?: number
}) => {
const { data } = useTypedSWR<UserProfile>(
shouldFetch && userId ? `/api/users/${userId}` : null
)
// data は UserProfile | undefined として正しく推論される
return (
<div>
{data ? (
<span>{data.name}</span>
) : (
<span>ユーザーが選択されていません</span>
)}
</div>
)
}
型推論の精度
複雑な型推論シナリオの比較
typescript// Zustand: 深い選択での型推論
interface ComplexState {
users: Record<string, User>;
posts: Record<string, Post[]>;
ui: {
modals: Record<string, boolean>;
loading: Record<string, boolean>;
};
}
const useComplexStore = create<
ComplexState & {
addUser: (user: User) => void;
toggleModal: (modalId: string) => void;
}
>((set) => ({
users: {},
posts: {},
ui: { modals: {}, loading: {} },
addUser: (user) =>
set((state) => ({
users: { ...state.users, [user.id]: user },
})),
toggleModal: (modalId) =>
set((state) => ({
ui: {
...state.ui,
modals: {
...state.ui.modals,
[modalId]: !state.ui.modals[modalId],
},
},
})),
}));
// 複雑な選択での正確な型推論
const ComplexSelector = ({
userId,
}: {
userId: string;
}) => {
const userPosts = useComplexStore((state) => {
const user = state.users[userId];
const posts = state.posts[userId] || [];
const isLoading =
state.ui.loading[`user-${userId}`] || false;
return {
user, // User | undefined として推論
posts, // Post[] として推論
isLoading, // boolean として推論
hasUser: user !== undefined, // boolean として推論
};
});
return (
<div>
{userPosts.hasUser && (
<div>
<h2>{userPosts.user.name}</h2>
{userPosts.isLoading ? (
<div>投稿を読み込み中...</div>
) : (
<div>{userPosts.posts.length} 件の投稿</div>
)}
</div>
)}
</div>
);
};
typescript// TanStack Query: 複雑なデータ変換での型推論
interface RawApiData {
users: Array<{
id: string;
firstName: string;
lastName: string;
posts: Array<{
id: string;
title: string;
publishedAt: string;
}>;
}>;
}
const useTransformedData = () => {
return useQuery({
queryKey: ['complex-data'],
queryFn: (): Promise<RawApiData> =>
fetch('/api/complex-data').then((res) => res.json()),
select: (data) => {
// 複雑なデータ変換
const transformedUsers = data.users.map((user) => ({
id: user.id,
fullName: `${user.firstName} ${user.lastName}`,
postCount: user.posts.length,
latestPost:
user.posts.sort(
(a, b) =>
new Date(b.publishedAt).getTime() -
new Date(a.publishedAt).getTime()
)[0] || null,
}));
const statistics = {
totalUsers: transformedUsers.length,
totalPosts: transformedUsers.reduce(
(sum, user) => sum + user.postCount,
0
),
activeUsers: transformedUsers.filter(
(user) => user.postCount > 0
).length,
};
// 戻り値の型は自動推論される
return {
users: transformedUsers,
stats: statistics,
};
},
});
};
// 使用時にも正確な型推論
const ComplexDataView = () => {
const { data, isLoading } = useTransformedData();
if (isLoading) return <div>読み込み中...</div>;
// data の型は select の戻り値として正確に推論される
return (
<div>
<h2>統計情報</h2>
<p>総ユーザー数: {data?.stats.totalUsers}</p>
<p>総投稿数: {data?.stats.totalPosts}</p>
<p>アクティブユーザー: {data?.stats.activeUsers}</p>
<h2>ユーザー一覧</h2>
{data?.users.map((user) => (
<div key={user.id}>
<h3>
{user.fullName} ({user.postCount} 投稿)
</h3>
{user.latestPost && (
<p>最新投稿: {user.latestPost.title}</p>
)}
</div>
))}
</div>
);
};
開発体験とコード補完
IDE サポートの質的比較
各ライブラリでの開発体験を、実際のコード補完例で比較してみましょう。
typescript// Zustand: 直感的なコード補完
const useShoppingCart = create<{
items: CartItem[];
total: number;
addItem: (item: Product, quantity: number) => void;
removeItem: (itemId: string) => void;
updateQuantity: (
itemId: string,
quantity: number
) => void;
clearCart: () => void;
}>((set, get) => ({
items: [],
total: 0,
addItem: (product, quantity) => {
set((state) => {
const existingItem = state.items.find(
(item) => item.product.id === product.id
);
if (existingItem) {
// IDE が existingItem の型を正確に推論し、補完を提供
return {
items: state.items.map((item) =>
item.product.id === product.id
? {
...item,
quantity: item.quantity + quantity,
}
: item
),
};
}
return {
items: [
...state.items,
{ product, quantity, id: generateId() },
],
};
});
},
// 他のメソッドも同様に型安全...
}));
// 使用時の優れたコード補完体験
const ShoppingCartComponent = () => {
const {
items, // CartItem[] として補完
total, // number として補完
addItem, // (item: Product, quantity: number) => void として補完
clearCart, // () => void として補完
} = useShoppingCart();
// state.items. を入力すると、配列メソッドが正確に補完される
const itemCount = items.length;
const hasItems = items.some((item) => item.quantity > 0);
return <div>{/* コンポーネントの実装 */}</div>;
};
typescript// TanStack Query: 高度な型推論とコード補完
const useProductWithRelated = (productId: number) => {
// メインの商品データ
const productQuery = useQuery({
queryKey: ['product', productId],
queryFn: () => fetchProduct(productId),
enabled: productId > 0, // 条件付きフェッチング
});
// 関連商品データ(メイン商品に依存)
const relatedQuery = useQuery({
queryKey: [
'related-products',
productQuery.data?.category,
],
queryFn: () =>
fetchRelatedProducts(productQuery.data!.category),
enabled: !!productQuery.data?.category, // productQuery.data の存在確認
});
// レビューデータ
const reviewsQuery = useQuery({
queryKey: ['reviews', productId],
queryFn: () => fetchReviews(productId),
enabled: productId > 0,
});
// 統合されたデータと状態
return {
product: productQuery.data,
relatedProducts: relatedQuery.data,
reviews: reviewsQuery.data,
isLoading:
productQuery.isLoading || relatedQuery.isLoading,
error:
productQuery.error ||
relatedQuery.error ||
reviewsQuery.error,
// すべてのプロパティが正確に型推論される
};
};
// カスタムフックの使用
const ProductDetailPage = ({
productId,
}: {
productId: number;
}) => {
const {
product, // Product | undefined として補完
relatedProducts, // Product[] | undefined として補完
reviews, // Review[] | undefined として補完
isLoading, // boolean として補完
error, // Error | null として補完
} = useProductWithRelated(productId);
// IDE が各プロパティの型を正確に認識し、適切な補完を提供
if (error) {
return <div>エラー: {error.message}</div>; // error.message が補完される
}
return (
<div>
{product && (
<div>
<h1>{product.name}</h1>{' '}
{/* product. で名前が補完される */}
<p>価格: ¥{product.price.toLocaleString()}</p>
</div>
)}
{relatedProducts && (
<div>
<h2>関連商品</h2>
{relatedProducts.map(
(
item // item の型も Product として推論
) => (
<div key={item.id}>{item.name}</div>
)
)}
</div>
)}
</div>
);
};
typescript// SWR: シンプルながら的確な型補完
interface PaginatedResponse<T> {
data: T[]
totalCount: number
hasMore: boolean
nextCursor?: string
}
const usePaginatedSWR = <T>(
baseUrl: string,
cursor?: string
) => {
const url = cursor ? `${baseUrl}?cursor=${cursor}` : baseUrl
const { data, error, mutate, isValidating } = useSWR<PaginatedResponse<T>>(
url,
typedFetcher<PaginatedResponse<T>>
)
// 型安全なページングロジック
const loadMore = useCallback(() => {
if (data?.hasMore && data.nextCursor) {
// 次のページをプリフェッチ
mutate(
typedFetcher<PaginatedResponse<T>>(`${baseUrl}?cursor=${data.nextCursor}`),
false
)
}
}, [data, baseUrl, mutate])
return {
items: data?.data || [], // T[] として推論
totalCount: data?.totalCount || 0, // number として推論
hasMore: data?.hasMore || false, // boolean として推論
isLoading: !data && !error, // boolean として推論
isValidating, // boolean として推論
error, // Error | undefined として推論
loadMore, // () => void として推論
}
}
// 使用例での優れたコード補完
const ProductPagination = () => {
const [cursor, setCursor] = useState<string>()
const {
items, // Product[] として補完
hasMore, // boolean として補完
isLoading, // boolean として補完
loadMore // () => void として補完
} = usePaginatedSWR<Product>('/api/products', cursor)
return (
<div>
{items.map(product => ( // product は Product 型として推論
<div key={product.id}>
<h3>{product.name}</h3> {/* product. で適切な補完 */}
<p>¥{product.price.toLocaleString()}</p>
</div>
))}
{hasMore && (
<button
onClick={loadMore} // loadMore の型が正確に推論される
disabled={isLoading}
>
{isLoading ? '読み込み中...' : 'さらに読み込む'}
</button>
)}
</div>
)
}
実運用での性能評価
バンドルサイズの比較
詳細なバンドルサイズ分析
実際のプロジェクトでのバンドルサイズへの影響を詳しく見てみましょう。
ライブラリ | 圧縮前サイズ | gzip 圧縮後 | 機能密度 | 追加依存関係 |
---|---|---|---|---|
Zustand | 12.4KB | 2.9KB | ★★★★☆ | なし |
TanStack Query | 123KB | 39KB | ★★★★★ | React 18+ |
SWR | 13.2KB | 4.2KB | ★★★☆☆ | なし |
typescript// バンドルサイズの最適化例
// 1. Zustand - 最小構成
import { create } from 'zustand';
// 必要に応じてミドルウェアを個別インポート
import { persist } from 'zustand/middleware';
import { devtools } from 'zustand/middleware';
// 開発環境でのみ devtools を含める
const useOptimizedStore = create(
process.env.NODE_ENV === 'development'
? devtools(
persist(
(set) => ({
// ストア定義
}),
{ name: 'app-store' }
)
)
: persist(
(set) => ({
// ストア定義
}),
{ name: 'app-store' }
)
);
// 2. TanStack Query - 必要な機能のみインポート
import {
useQuery,
useMutation,
useQueryClient,
} from '@tanstack/react-query';
// 開発ツールは条件付きインポート
const ReactQueryDevtools =
process.env.NODE_ENV === 'development'
? require('@tanstack/react-query-devtools')
.ReactQueryDevtools
: () => null;
// 3. SWR - 必要な機能のみ使用
import useSWR from 'swr';
// 必要に応じて追加機能をインポート
import useSWRInfinite from 'swr/infinite';
import useSWRMutation from 'swr/mutation';
Tree Shaking の効果
typescript// 最適化前(非推奨)
import * as zustand from 'zustand';
import * as tanstackQuery from '@tanstack/react-query';
import * as swr from 'swr';
// 最適化後(推奨)
import { create } from 'zustand';
import {
useQuery,
useMutation,
} from '@tanstack/react-query';
import useSWR from 'swr';
// バンドルアナライザーでの確認方法
// 1. webpack-bundle-analyzer の使用
// 2. Next.js の場合
// next.config.js
module.exports = {
webpack: (config, { isServer }) => {
if (!isServer) {
config.resolve.fallback = {
fs: false,
path: false,
};
}
return config;
},
// バンドル分析を有効化
experimental: {
bundlePagesRouterDependencies: true,
},
};
レンダリング最適化
各ライブラリでのレンダリング最適化比較
レンダリング頻度とパフォーマンスの実測値を比較してみましょう。
typescript// Zustand - 選択的購読によるレンダリング最適化
interface AppState {
user: User | null;
posts: Post[];
ui: {
isLoading: boolean;
activeModal: string | null;
};
}
// 非効率な購読(アンチパターン)
const InefficientComponent = () => {
const state = useAppStore(); // 全状態を購読 → 頻繁な再レンダリング
return <div>{state.user?.name}</div>;
};
// 効率的な購読(ベストプラクティス)
const EfficientComponent = () => {
const userName = useAppStore((state) => state.user?.name); // 必要な部分のみ購読
return <div>{userName}</div>;
};
// 複雑な選択での最適化
const OptimizedListComponent = () => {
const filteredPosts = useAppStore(
(state) =>
state.posts.filter((post) => post.isPublished),
(a, b) => a.length === b.length // カスタム比較関数
);
return (
<ul>
{filteredPosts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
};
typescript// TanStack Query - 自動最適化とカスタム最適化
const OptimizedQueryComponent = () => {
const { data: posts } = useQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
select: (data) => {
// データ変換の結果のみが変更された場合に再レンダリング
return data
.filter((post) => post.status === 'published')
.sort(
(a, b) =>
new Date(b.createdAt).getTime() -
new Date(a.createdAt).getTime()
);
},
// 構造化比較による最適化
structuralSharing: true,
});
return (
<ul>
{posts?.map((post) => (
<PostItem key={post.id} post={post} />
))}
</ul>
);
};
// メモ化を活用した最適化
const PostItem = memo(({ post }: { post: Post }) => {
const { mutate: updatePost } = useMutation({
mutationFn: (updatedPost: Partial<Post>) =>
updatePostApi(post.id, updatedPost),
onSuccess: () => {
// 個別の投稿のみ無効化
queryClient.invalidateQueries({
queryKey: ['post', post.id],
});
},
});
return (
<li>
<h3>{post.title}</h3>
<button
onClick={() =>
updatePost({ isLiked: !post.isLiked })
}
>
{post.isLiked ? 'いいね済み' : 'いいね'}
</button>
</li>
);
});
typescript// SWR - 効率的なデータフェッチングとレンダリング
const OptimizedSWRComponent = () => {
const { data: posts } = useSWR('/api/posts', fetcher, {
// 不必要な再検証を抑制
revalidateOnFocus: false,
revalidateOnReconnect: false,
dedupingInterval: 5000,
});
// React.memo と組み合わせた最適化
const memoizedPosts = useMemo(() => {
return posts?.filter((post) => post.isVisible) || [];
}, [posts]);
return (
<div>
{memoizedPosts.map((post) => (
<MemoizedPostItem key={post.id} post={post} />
))}
</div>
);
};
const MemoizedPostItem = memo(
({ post }: { post: Post }) => {
const { trigger } = useSWRMutation(
`/api/posts/${post.id}`,
updatePostMutator
);
return (
<li>
<h3>{post.title}</h3>
<button
onClick={() =>
trigger({ isBookmarked: !post.isBookmarked })
}
>
{post.isBookmarked
? 'ブックマーク済み'
: 'ブックマーク'}
</button>
</li>
);
},
(prevProps, nextProps) => {
// カスタム比較関数
return (
prevProps.post.id === nextProps.post.id &&
prevProps.post.title === nextProps.post.title &&
prevProps.post.isBookmarked ===
nextProps.post.isBookmarked
);
}
);
レンダリングパフォーマンス測定
typescript// パフォーマンス測定のための実装例
const PerformanceTracker = ({
children,
}: {
children: React.ReactNode;
}) => {
const renderCount = useRef(0);
const startTime = useRef(Date.now());
renderCount.current++;
useEffect(() => {
const endTime = Date.now();
const renderTime = endTime - startTime.current;
console.log(
`レンダリング回数: ${renderCount.current}, 時間: ${renderTime}ms`
);
// 本番環境では分析サービスに送信
if (process.env.NODE_ENV === 'production') {
analytics.track('component_render', {
renderCount: renderCount.current,
renderTime,
component: 'DataList',
});
}
});
return <>{children}</>;
};
// 使用例
const TrackedDataList = () => (
<PerformanceTracker>
<DataListComponent />
</PerformanceTracker>
);
複雑な状態管理での比較
大規模アプリケーションでの実装比較
実際の管理画面レベルの複雑さで、各ライブラリがどのように動作するかを比較してみましょう。
typescript// 複雑な状態管理のシナリオ
interface DashboardState {
// ユーザー関連
currentUser: User | null;
users: Record<string, User>;
// データ関連
products: Record<string, Product>;
orders: Record<string, Order>;
analytics: AnalyticsData | null;
// UI状態
filters: {
dateRange: [Date, Date];
category: string[];
status: OrderStatus[];
};
// 非同期状態
loadingStates: Record<string, boolean>;
errors: Record<string, Error>;
}
Zustand での大規模状態管理
typescript// 状態のスライシング(推奨パターン)
const useUserSlice = create<{
currentUser: User | null;
users: Record<string, User>;
setCurrentUser: (user: User) => void;
addUser: (user: User) => void;
}>((set) => ({
currentUser: null,
users: {},
setCurrentUser: (user) => set({ currentUser: user }),
addUser: (user) =>
set((state) => ({
users: { ...state.users, [user.id]: user },
})),
}));
const useProductSlice = create<{
products: Record<string, Product>;
addProduct: (product: Product) => void;
updateProduct: (
id: string,
updates: Partial<Product>
) => void;
}>((set) => ({
products: {},
addProduct: (product) =>
set((state) => ({
products: {
...state.products,
[product.id]: product,
},
})),
updateProduct: (id, updates) =>
set((state) => ({
products: {
...state.products,
[id]: { ...state.products[id], ...updates },
},
})),
}));
const useUISlice = create<{
filters: DashboardState['filters'];
updateFilters: (
updates: Partial<DashboardState['filters']>
) => void;
}>((set) => ({
filters: {
dateRange: [new Date(), new Date()],
category: [],
status: [],
},
updateFilters: (updates) =>
set((state) => ({
filters: { ...state.filters, ...updates },
})),
}));
// 複合コンポーネントでの使用
const ComplexDashboard = () => {
const { currentUser } = useUserSlice();
const { products, updateProduct } = useProductSlice();
const { filters, updateFilters } = useUISlice();
// 複数のスライスからデータを組み合わせ
const filteredProducts = Object.values(products).filter(
(product) => {
return (
filters.category.length === 0 ||
filters.category.includes(product.category)
);
}
);
return (
<div>
<UserHeader user={currentUser} />
<FilterControls
filters={filters}
onUpdateFilters={updateFilters}
/>
<ProductGrid
products={filteredProducts}
onUpdateProduct={updateProduct}
/>
</div>
);
};
TanStack Query での大規模データ管理
typescript// クエリキーの体系的管理
const queryKeys = {
users: {
all: ['users'] as const,
lists: () => [...queryKeys.users.all, 'list'] as const,
list: (filters: UserFilters) =>
[...queryKeys.users.lists(), filters] as const,
details: () =>
[...queryKeys.users.all, 'detail'] as const,
detail: (id: string) =>
[...queryKeys.users.details(), id] as const,
},
products: {
all: ['products'] as const,
lists: () =>
[...queryKeys.products.all, 'list'] as const,
list: (filters: ProductFilters) =>
[...queryKeys.products.lists(), filters] as const,
details: () =>
[...queryKeys.products.all, 'detail'] as const,
detail: (id: string) =>
[...queryKeys.products.details(), id] as const,
},
analytics: {
all: ['analytics'] as const,
summary: (params: AnalyticsParams) =>
[
...queryKeys.analytics.all,
'summary',
params,
] as const,
},
} as const;
// 複雑なデータ関係の管理
const useDashboardData = (filters: DashboardFilters) => {
// 基本データの取得
const usersQuery = useQuery({
queryKey: queryKeys.users.list(filters.userFilters),
queryFn: () => fetchUsers(filters.userFilters),
staleTime: 5 * 60 * 1000,
});
const productsQuery = useQuery({
queryKey: queryKeys.products.list(
filters.productFilters
),
queryFn: () => fetchProducts(filters.productFilters),
staleTime: 5 * 60 * 1000,
});
// 依存関係のあるデータ
const analyticsQuery = useQuery({
queryKey: queryKeys.analytics.summary({
userIds: usersQuery.data?.map((u) => u.id) || [],
productIds:
productsQuery.data?.map((p) => p.id) || [],
dateRange: filters.dateRange,
}),
queryFn: ({ queryKey }) =>
fetchAnalytics(queryKey[2] as AnalyticsParams),
enabled: !!(usersQuery.data && productsQuery.data),
staleTime: 1 * 60 * 1000, // より頻繁に更新
});
// 集約データの計算
const aggregatedData = useMemo(() => {
if (
!usersQuery.data ||
!productsQuery.data ||
!analyticsQuery.data
) {
return null;
}
return {
totalRevenue: analyticsQuery.data.revenue,
topUsers: usersQuery.data
.sort((a, b) => b.totalSpent - a.totalSpent)
.slice(0, 5),
topProducts: productsQuery.data
.sort((a, b) => b.salesCount - a.salesCount)
.slice(0, 5),
};
}, [
usersQuery.data,
productsQuery.data,
analyticsQuery.data,
]);
return {
users: usersQuery.data,
products: productsQuery.data,
analytics: analyticsQuery.data,
aggregatedData,
isLoading:
usersQuery.isLoading ||
productsQuery.isLoading ||
analyticsQuery.isLoading,
error:
usersQuery.error ||
productsQuery.error ||
analyticsQuery.error,
};
};
// 複雑な更新操作の管理
const useComplexMutations = () => {
const queryClient = useQueryClient();
const updateProductMutation = useMutation({
mutationFn: ({
id,
updates,
}: {
id: string;
updates: Partial<Product>;
}) => updateProduct(id, updates),
onMutate: async ({ id, updates }) => {
// 関連するクエリをキャンセル
await queryClient.cancelQueries({
queryKey: queryKeys.products.all,
});
// 楽観的更新
const previousData = queryClient.getQueryData(
queryKeys.products.detail(id)
);
queryClient.setQueryData(
queryKeys.products.detail(id),
(old: Product) => ({
...old,
...updates,
})
);
return { previousData };
},
onSuccess: (updatedProduct) => {
// 関連するクエリを無効化
queryClient.invalidateQueries({
queryKey: queryKeys.products.lists(),
});
queryClient.invalidateQueries({
queryKey: queryKeys.analytics.all,
});
// 詳細データを直接更新
queryClient.setQueryData(
queryKeys.products.detail(updatedProduct.id),
updatedProduct
);
},
onError: (err, { id }, context) => {
// ロールバック
if (context?.previousData) {
queryClient.setQueryData(
queryKeys.products.detail(id),
context.previousData
);
}
},
});
return { updateProduct: updateProductMutation.mutate };
};
SWR での大規模データ管理
typescript// SWR での体系的なデータ管理
const useComplexSWRData = (filters: DashboardFilters) => {
// 基本データの取得
const { data: users } = useSWR(
`/api/users?${new URLSearchParams(
filters.userFilters
).toString()}`,
fetcher,
{
revalidateOnFocus: false,
dedupingInterval: 5000,
}
);
const { data: products } = useSWR(
`/api/products?${new URLSearchParams(
filters.productFilters
).toString()}`,
fetcher,
{
revalidateOnFocus: false,
dedupingInterval: 5000,
}
);
// 依存関係のあるデータ(条件付きフェッチング)
const analyticsKey =
users && products
? `/api/analytics?userIds=${users
.map((u) => u.id)
.join(',')}&productIds=${products
.map((p) => p.id)
.join(
','
)}&startDate=${filters.dateRange[0].toISOString()}&endDate=${filters.dateRange[1].toISOString()}`
: null;
const { data: analytics } = useSWR(
analyticsKey,
fetcher,
{
refreshInterval: 60000, // 1分ごとに更新
revalidateOnFocus: true, // 分析データはフォーカス時に更新
}
);
// データの統合と変換
const dashboardData = useMemo(() => {
if (!users || !products || !analytics) {
return null;
}
return {
summary: {
totalUsers: users.length,
totalProducts: products.length,
totalRevenue: analytics.totalRevenue,
},
trends: analytics.trends,
topPerformers: {
users: users
.sort((a, b) => b.totalSpent - a.totalSpent)
.slice(0, 10),
products: products
.sort((a, b) => b.salesCount - a.salesCount)
.slice(0, 10),
},
};
}, [users, products, analytics]);
return {
users,
products,
analytics,
dashboardData,
isLoading: !users || !products || !analytics,
};
};
// 複雑な更新操作とキャッシュ管理
const useOptimisticUpdates = () => {
const { mutate: globalMutate } = useSWRConfig();
const updateProductWithOptimisticUpdate = useCallback(
async (
productId: string,
updates: Partial<Product>
) => {
const productKey = `/api/products/${productId}`;
const productsListKey = '/api/products';
try {
// 楽観的更新
globalMutate(
productKey,
(currentProduct: Product) => ({
...currentProduct,
...updates,
}),
false
);
// 商品リストも楽観的更新
globalMutate(
(key) =>
typeof key === 'string' &&
key.startsWith('/api/products'),
(currentData: Product[]) =>
currentData?.map((product) =>
product.id === productId
? { ...product, ...updates }
: product
),
false
);
// 実際のAPI呼び出し
const updatedProduct = await updateProduct(
productId,
updates
);
// 成功時は実際のデータで更新
globalMutate(productKey, updatedProduct, false);
// 関連するキャッシュを再検証
globalMutate(
(key) =>
typeof key === 'string' &&
key.startsWith('/api/analytics'),
undefined,
{ revalidate: true }
);
return updatedProduct;
} catch (error) {
// エラー時は元のデータを再フェッチ
globalMutate(productKey);
globalMutate(productsListKey);
throw error;
}
},
[globalMutate]
);
return {
updateProduct: updateProductWithOptimisticUpdate,
};
};
まとめ
本記事では、Zustand、TanStack Query、SWR の 3 つのライブラリについて、キャッシュ戦略、再検証メカニズム、型安全性、実運用での性能の観点から詳細に比較検証しました。
各ライブラリの特徴まとめ
Zustand は、シンプルさと軽量性を重視する場合に最適です。バンドルサイズが小さく、学習コストが低いため、小〜中規模のプロジェクトや、複雑でないグローバル状態管理が必要な場合に威力を発揮します。TypeScript との親和性も高く、型安全性を保ちながら直感的な開発が可能です。
TanStack Query は、サーバー状態管理において最も包括的な機能を提供します。複雑なキャッシュ戦略、自動的な再検証、楽観的更新など、データドリブンなアプリケーションに必要な機能が充実しています。バンドルサイズは大きめですが、その分多機能で、大規模なプロジェクトでの運用実績も豊富です。
SWR は、シンプルながら効果的なデータフェッチングソリューションを提供します。Next.js との親和性が高く、Stale-While-Revalidate 戦略により優れたユーザー体験を実現できます。学習コストが低く、素早い開発が可能です。
プロジェクト選定の指針
- 小〜中規模プロジェクト、シンプルな状態管理 → Zustand
- 大規模プロジェクト、複雑なサーバー状態管理 → TanStack Query
- Next.js プロジェクト、素早い開発 → SWR
- バンドルサイズを重視 → Zustand または SWR
- 高度なキャッシュ制御が必要 → TanStack Query
実際のプロジェクトでは、これらのライブラリを組み合わせて使用することも可能です。例えば、Zustand でクライアント状態を管理し、TanStack Query でサーバー状態を管理するハイブリッド構成も効果的な選択肢となります。
最終的な選択は、チームの技術レベル、プロジェクトの要件、将来の拡張性を総合的に考慮して決定することが重要です。
関連リンク
- article
Zustand × TanStack Query × SWR:キャッシュ・再検証・型安全の実運用比較
- article
Zustand × Next.js の Hydration Mismatch を根絶する:原因別チェックリスト
- article
Zustand を React なしで使う:subscribe と Store API だけで組む最小構成
- article
Zustand の状態管理を使ったカスタムフック作成術
- article
Zustand × URL クエリパラメータ連動:状態同期で UX を高める
- article
Zustand のストア構造を大型プロジェクト向けに最適化する手法
- article
MySQL Optimizer Hints 実測比較:INDEX_MERGE/NO_RANGE_OPTIMIZATION ほか
- article
Zustand × TanStack Query × SWR:キャッシュ・再検証・型安全の実運用比較
- article
Motion(旧 Framer Motion) vs CSS Transition/WAAPI:可読性・制御性・性能を実測比較
- article
WordPress 情報設計:CPT/タクソノミー/メタデータの設計指針
- article
WebSocket vs WebTransport vs SSE 徹底比較:遅延・帯域・安定性を実測レビュー
- article
JavaScript OffscreenCanvas 検証:Canvas/OffscreenCanvas/WebGL の速度比較
- blog
iPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
- blog
Googleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
- blog
【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
- blog
Googleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
- blog
Pixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
- blog
フロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
- review
今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
- review
ついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
- review
愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
- review
週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
- review
新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
- review
科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来