data-fetching-architecture について
LobeHub のデータ取得フローを実装・移行するためのスキルです。サービス、Store、SWR、キャッシュ、更新処理、画面状態に関する規約を開発者向けにまとめています。
主な機能
- 01サービス層とStoreの責務分担
- 02SWRフックとキャッシュキーの規約
- 03更新処理と楽観的更新の指針
- 04読み込み・エラー・空状態の処理
- 05既存の取得処理の移行手順
活用シーン
- 01Store管理の読み取りフックの追加
- 02書き込み後の関連キャッシュ更新
- 03リスト取得の失敗と再試行への対応
- 04コンポーネント内の取得処理をサービスとStoreへ移行
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ lobehub/ lobehub --skill data-fetching-architectureインストール後、Claude Code で/data-fetching-architectureを入力して読み込まれたか確認します。
同梱ファイルreferences/home-first-paint.mdreferences/walkthrough.md
詳細
LobeHub のデータ取得アーキテクチャ
Component → Store useFetchXxx hook → Service → lambdaClient
← SWR request state + store data ← response
レイヤーの境界
src/services/のサービスは API 呼び出しを担当します。読み取りには.query()、書き込みには.mutate()を使用します。 ドメインごとにサービスインスタンスをエクスポートし、コンポーネントやストアからlambdaClientを直接呼び出さないでください。- ストアの読み取りフックでは
useClientDataSWRを使用し、errorやmutateを含む SWR レスポンスを返します。成功した結果は、ラッパーがサポートする コールバック(onSuccess、同期ラッパーの場合はonData)を通じてストアに同期します。 - ローカルコピーから描画するサーバーリストや詳細情報には、代わりに
@/libs/replicaを使用します。ストアのuseFetchXxxはslice.useSync(params)(フラグのみ)を返し、エンジンがストアに書き込みます。zustand の Replica セクションを参照してください。 - コンポーネントはこれらのフックを呼び出し、セレクターを通じてストアのデータを読み取ります。
useEffectで取得したり、コンポーネントのuseStateにサーバーデータを重複して保持したりしないでください。 - 読み取りフックには
useFetchXxxを、キャッシュの無効化にはrefreshXxxを使用します。 - リスト/詳細の型、マップ、リデューサー、共有型のソースには、 Zustand のデータ構造を使用してください。ストアの構成とアクションクラスについては、zustandを参照してください。
ホームの初回描画と永続キャッシュ
ホーム/サイドバーの初回描画や永続化された表示データを変更する場合は、
references/home-first-paint.mdを読んでください。ちらつきの回避、永続化の再利用、ユーザーの displaySnapshot.ts の境界について説明しています。
キャッシュキーと更新
src/libs/swr/keys.ts のドメインキー生成関数を再利用してください。ドメインにキーがない場合は、共有キーを定義します。レスポンスを変えるすべてのパラメーター(エンティティまたは親の ID、フィルター、並べ替え、ページネーション)を含めてください。読み取り時と更新時には、同じキーを構築する必要があります。
アプリケーションのワークスペース/キャッシュ処理を維持するため、@/libs/swr から useClientDataSWR と mutate をインポートしてください。必須の ID がない場合は null キーを返します。ストアフックに undefined を渡しても、そのフックが null に変換する場合に限り、取得が無効になります。
たとえば、アクションクラスでは次のようにします。
useFetchBenchmarks = () =>
useClientDataSWR(evalKeys.benchmarks(), () => agentEvalService.listBenchmarks(), {
onSuccess: (data) => {
this.#set({ benchmarkList: data, benchmarkListInit: true });
},
});
refreshBenchmarks = async () => {
await mutate(evalKeys.benchmarks());
};
ストアとの接続については、現在のベンチマークアクションを確認してください。実際のオプションについては、SWR ラッパーを参照してください。これらのリンクは実装箇所を示すものであり、既存の呼び出し箇所を変更せずにそのままコピーするためのテンプレートではありません。
既知の親 ID を共有するフックは、まとめて実行できます。あるリクエストが別のリクエストの返す値を必要とする場合、その値が得られるまでキーを null に保ってください。アプリケーションのラッパーが、アップストリームの SWR 既定の重複排除間隔を使用すると想定しないでください。
複数の親の下で別々にキャッシュされるリストについては、 親キー付きリストを参照してください。通常のフラットなリストには、追加のレイヤーは不要です。
ミューテーション
- サービスを呼び出してから、影響を受けるリスト/詳細のキーと、レスポンスが変化した関連ドメインを更新します。後続の UI の動作が更新結果に依存する場合は、更新を
awaitしてください。 - 保留中フラグはストアの状態に保持し、
finallyでクリアします。更新/削除では ID ごとの状態を使用し、関係のない行を引き続き操作できるようにします。永続的な ID がまだ存在しないため、作成には別のフラグを使用できます。 - レプリカデータへの楽観的な書き込みには
slice.optimistic/entity.optimisticを使用します。呼び出しが拒否されると、削除を含め、自動的にロールバックされます。 - レガシーストアでの楽観的な作成/更新には、ストアのリデューサー/ディスパッチの規約を使用し、サービスが失敗した場合は影響を受けた状態を復元するか再検証します。拒否後に一時的な行や、成功したように見える編集を残さないでください。手書きのリデューサーにはロールバック用の基準状態がないため、その場合はサーバーの成功後にのみ削除してください。
- エラーは呼び出し元のエラー UI まで伝播させてください。
finallyブロックで保留中状態をクリアしても、それだけでは楽観的な書き込みは復元されません。
読み込み、エラー、確定済みデータの表示
読み取りフックの SWR レスポンスを使用します。isInit のような成功時のみ設定されるフラグ、存在しないマップエントリ、data ?? [] では、初回リクエストの失敗と読み込み中、結果が空の場合を区別できません。
標準的な読み込み/エラー/空/データの表示には AsyncBoundary を使用し、カスタムレイアウトやインラインエラーには AsyncError を使用します。元の SWR data を境界に渡します。undefined は成功した結果がないことを意味し、[] は結果が確定していて空であることを意味します。
Replica の useSync は data を返しません。error と、ストアのエントリから得られる確定済みシグナル(セレクターが entry !== undefined の場合、data={isListInit || undefined})を AsyncBoundary に渡します。src/features/Pages/PageLayout/Body/index.tsx を参照してください。
const BenchmarkList = () => {
const useFetchBenchmarks = useEvalStore((s) => s.useFetchBenchmarks);
const benchmarks = useEvalStore((s) => s.benchmarkList);
const { data, error, isLoading, mutate } = useFetchBenchmarks();
return (
<AsyncBoundary
data={data}
empty={<EmptyState />}
error={error}
isEmpty={data?.length === 0}
isLoading={isLoading}
onRetry={() => {
void mutate();
}}
>
<BenchmarkCards items={benchmarks} />
</AsyncBoundary>
);
};
例の EmptyState と BenchmarkCards は、サーフェスに既存のレンダラーを表しています。これに合わせて、次の点を考慮してください。
- 初回読み込み時のエラーは、空/
NotFound/ゼロ値のデフォルトより先に表示します。if (!map[id]) return <Skeleton />の後にエラーブランチを置かないでください。失敗してもマップに値が入らないことがあります。 - バックグラウンドでの再検証が失敗しても、正常に読み込まれた空のリストを含め、確定済みのコンテンツを維持します。再試行中は、処理中であることがわかるフィードバックを表示します。
- 取得データと静的データを組み合わせたリストでは、行を結合する前に、取得データ部分のリクエスト状態を確認します。静的な行によってリクエストの失敗が隠れないようにします。
- 無限スクロールでは、バケットごとに
loadMoreErrorを保持し、インラインの再試行行を表示します。そのエラーが解消されるまで、オブザーバーによる再試行を一時停止します。 - 閉じたモーダルや存在しない ID はリクエストを無効にするものであり、レコードが見つからないことを示すものではありません。
既存のフェッチ処理の移行
API 呼び出しをサービスに移し、リクエストをストアの SWR フックに移します。タスク上変更が必要な場合を除き、既存の状態の形とアクションの構成を再利用します。コンポーネントのエフェクト/ローカルなフェッチ状態をそのフックとセレクターに置き換え、error、再試行、処理中のフィードバックを接続します。書き込み後は、同じキャッシュキーを更新します。
変更対象のサーフェスで、初回の失敗、再試行、確定済みの空の結果、バックグラウンドでの失敗、ID の切り替えを確認します。古いデータが表示される場合は、読み取り/更新キーと、それらが更新するストアのバケットを比較します。読み込みが終わらない場合は、成功時のコールバックだけでなく、拒否されたリクエストも調べます。原因を覆い隠すために、2 つ目のフェッチや別の読み込みフラグを追加しないでください。