Zustandでの非同期処理とfetch連携パターン(パターン1: 基本的なデータフェッチング)

Zustandで非同期処理を実装する際の基本パターンとして、データフェッチングは最も一般的なユースケースです。この記事では、Zustandを使った基本的なデータフェッチングパターンについて詳しく解説します。
パターン 1: 基本的なデータフェッチング
最も一般的な非同期パターンは、コンポーネントがマウントされたときにデータを取得するというものです。Zustand を使った基本的なデータフェッチングパターンを詳しく見ていきましょう。
ユースケース: ユーザープロフィールの取得
ユーザープロフィールを取得し、表示するユースケースを考えてみましょう:
typescript// src/stores/userStore.ts
import { create } from 'zustand';
interface User {
id: string;
name: string;
email: string;
avatarUrl: string;
}
interface UserStore {
// 状態
user: User | null;
isLoading: boolean;
error: string | null;
// アクション
fetchUser: (id: string) => Promise<void>;
clearUser: () => void;
}
export const useUserStore = create<UserStore>((set) => ({
user: null,
isLoading: false,
error: null,
fetchUser: async (id) => {
// リクエスト開始前にローディング状態をセット
set({ isLoading: true, error: null });
try {
// APIリクエスト
const response = await fetch(`/api/users/${id}`);
// エラーレスポンスのチェック
if (!response.ok) {
throw new Error(
`Failed to fetch user: ${response.statusText}`
);
}
// データの解析
const userData = await response.json();
// 成功した場合、状態を更新
set({ user: userData, isLoading: false });
} catch (error) {
// エラーをキャプチャして状態に保存
set({
error:
error instanceof Error
? error.message
: '未知のエラーが発生しました',
isLoading: false,
});
}
},
clearUser: () => set({ user: null, error: null }),
}));
コンポーネントとの連携
このストアを React コンポーネントで使用する例を見てみましょう:
tsx// src/components/UserProfile.tsx
import React, { useEffect } from 'react';
import { useUserStore } from '../stores/userStore';
interface UserProfileProps {
userId: string;
}
export const UserProfile: React.FC<UserProfileProps> = ({
userId,
}) => {
const { user, isLoading, error, fetchUser } =
useUserStore();
useEffect(() => {
// コンポーネントマウント時またはuserIdが変更されたときにデータを取得
fetchUser(userId);
// クリーンアップ関数
return () => {
useUserStore.getState().clearUser();
};
}, [userId]);
if (isLoading) {
return (
<div className='loading-spinner'>読み込み中...</div>
);
}
if (error) {
return (
<div className='error-message'>エラー: {error}</div>
);
}
if (!user) {
return <div>ユーザー情報がありません</div>;
}
return (
<div className='user-profile'>
<img
src={user.avatarUrl}
alt={`${user.name}のプロフィール画像`}
className='avatar'
/>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
};
ポイント
この基本パターンには、いくつかの重要なポイントがあります:
-
ローディング状態の追跡: データ取得の開始時にローディング状態を true に設定し、完了時(成功またはエラー)に false に戻します。
-
エラーハンドリング: エラーメッセージを状態の一部として保存し、ユーザーに表示します。
-
クリーンアップ: コンポーネントのアンマウント時にストア状態をクリアして、不要なデータを残さないようにします。
-
再フェッチ条件: 依存配列(この例では
[userId]
)を使って、いつデータを再取得するかを制御します。
実装のバリエーション
基本的なデータフェッチングパターンには、いくつかのバリエーションがあります:
1. 自動フェッチングと手動フェッチング
上記の例では、コンポーネントのマウント時に自動的にデータを取得していますが、ボタンクリックなどのユーザーアクションに応じて手動でデータを取得することもできます:
tsx// 手動フェッチングの例
const UserProfileWithButton = ({ userId }) => {
const { user, isLoading, error, fetchUser } = useUserStore();
return (
<div>
<button
onClick={() => fetchUser(userId)}
disabled={isLoading}
>
ユーザー情報を取得
</button>
{/* ユーザー情報の表示 */}
{user && <UserInfo user={user} />}
{isLoading && <LoadingIndicator />}
{error && <ErrorMessage message={error} />}
</div>
);
};
2. 初期データの提供
ページの初期ロード時にサーバーからデータを受け取る場合、それを初期状態として設定できます:
typescript// 初期データを受け入れるストア
const useUserStore = create<UserStore>((set) => ({
user: null, // 後で初期化可能
isLoading: false,
error: null,
initializeWithData: (initialData) => {
set({ user: initialData });
},
// その他のアクション
}));
// 使用例(Next.jsなどのSSRフレームワークで)
export const UserProfileWithInitialData = ({ initialUserData }) => {
const { user, initializeWithData } = useUserStore();
useEffect(() => {
if (initialUserData) {
initializeWithData(initialUserData);
}
}, [initialUserData]);
// 以下、通常のレンダリングロジック
};
3. 条件付きフェッチング
特定の条件が満たされた場合にのみデータを取得する例:
typescriptconst useConditionalFetchStore = create((set, get) => ({
data: null,
isLoading: false,
error: null,
lastFetched: null,
fetchIfNeeded: async (id) => {
const { data, lastFetched } = get();
const currentTime = Date.now();
// 以下の場合にのみ取得:
// 1. データがまだない
// 2. 最後の取得から5分以上経過している
// 3. IDが変更された
if (
!data ||
!lastFetched ||
currentTime - lastFetched > 5 * 60 * 1000 ||
data.id !== id
) {
// 通常のフェッチ処理
set({ isLoading: true });
try {
const response = await fetch(`/api/data/${id}`);
const newData = await response.json();
set({
data: newData,
lastFetched: Date.now(),
isLoading: false
});
} catch (error) {
set({ error: error.message, isLoading: false });
}
}
}
}));
まとめ
この記事では、Zustandを使った基本的なデータフェッチングパターンについて解説しました。Zustandのシンプルさと柔軟性を活かした実装により、非同期データ取得を効率的に管理できることが分かりました。
基本的なデータフェッチングパターンの重要なポイントは以下の通りです:
- 状態設計: データ、ローディング状態、エラー状態を適切に定義する
- 非同期アクション: async/await を使った直感的な実装
- エラーハンドリング: try/catch による包括的なエラー処理
- コンポーネント連携: useEffect または手動トリガーによる適切なタイミングでのデータ取得
- クリーンアップ: 不要になったデータの適切な処理
このパターンは、より複雑な非同期シナリオ(楽観的更新、無限スクロール、WebSocketなど)の基盤となる重要な実装方法です。
関連リンク
- Zustand 公式ドキュメント
- React Query - より高度なデータフェッチング機能を提供するライブラリ
- SWR - React Hooks ベースのデータフェッチングライブラリ
- MDN - Fetch API
- MDN - Promise