はじめに
前回
フロントエンド性能は キャッシュ と データ整合性 のバランスで決まる。React Query(RQ)と Suspense を組み合わせれば、ネットワーク遅延を隠蔽しつつ型安全を担保できる。本稿では、実務アプリで採用される 3 種のキャッシュ戦略を TypeScript 型と併せて解説する。
1. 基本構成
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { trpc } from "../utils/trpc";
const queryClient = new QueryClient({
defaultOptions: {
queries: { suspense: true, retry: 1 },
},
});
export function App() {
return (
<QueryClientProvider client={queryClient}>
<trpc.Provider client={trpcClient} queryClient={queryClient}>
<Root />
</trpc.Provider>
</QueryClientProvider>
);
}
-
suspense: true
でローディング UI を自動化する。 - tRPC との連携で型安全 API クライアントを構築。
2. キャッシュ戦略 3 パターン
戦略 | 目的 | 設定例 |
---|---|---|
Stale-While-Revalidate | 速い初期描画+バックグラウンド更新 | staleTime: 0, refetchOnWindowFocus: true |
Freshness First | 重要データを必ず最新化 | staleTime: 0, refetchOnMount: "always" |
Cache-Only | 頻繁参照・不変データを高速提供 | staleTime: Infinity, cacheTime: Infinity |
2.1 実装例(Stale-While-Revalidate)
const { data: user } = trpc.getUser.useQuery({ id }, {
staleTime: 10 * 60 * 1000, // 10 分は新鮮扱い
});
- 10 分以内の再レンダリングはキャッシュのみ。
- 背景更新は無駄なローディングを発生させない。
3. 型安全なキャッシュキー設計
const key = ["user", id] as const; // リテラル保持
queryClient.prefetchQuery(key, () => getUser(id));
-
as const
でキーをリテラル型に固定し、再利用時のミスを防ぐ。 - ユニオンキーの型を
type QueryKey = readonly ["user", number]
として共有すると IDE 補完が有効。
4. 楽観的更新(Optimistic Update)
trpc.updateUser.useMutation({
onMutate: async (input) => {
await queryClient.cancelQueries(["user", input.id]);
const prev = queryClient.getQueryData<User>(["user", input.id]);
if (prev) {
queryClient.setQueryData(["user", input.id], { ...prev, ...input });
}
return { prev };
},
onError: (_err, _input, ctx) => {
if (ctx?.prev) queryClient.setQueryData(["user", _input.id], ctx.prev);
},
onSettled: (_d, _e, input) => {
queryClient.invalidateQueries(["user", input.id]);
},
});
- 型引数
User
を付与し、更新ロジックで型安全性を維持。 - エラー時には前状態をロールバック。
5. Suspense + エラーバウンダリ
function UserPage({ id }: { id: number }) {
return (
<ErrorBoundary>
<Suspense fallback={<Spinner />}>
<UserDetail id={id} />
</Suspense>
</ErrorBoundary>
);
}
- Suspense でローディングを宣言的に扱い、UI コードの分岐を削減。
- ErrorBoundary で tRPC エラー型を捕捉し、個別 UI を出し分け。
6. ベンチマーク
構成 | FCP | データ再取得 | 型安全 |
---|---|---|---|
fetch + useState | 1.2 s | 1.2 s | △ 手書き型 |
RQ + tRPC + Suspense | 0.4 s | 3 ms (キャッシュ) | ◎ 型推論自動 |
7. 落とし穴と対策
落とし穴 | 原因 | 対策 |
---|---|---|
キャッシュ一意性衝突 | キー設計が曖昧 |
as const & 型定義でキー統一 |
過剰リフェッチ |
staleTime 未設定 |
適切な期限 or cacheTime を長めに取る |
楽観的更新の型不整合 | 部分更新時に型安全が崩壊 |
Partial<User> を使い入力 DTO を分離 |
まとめ
- キャッシュ戦略 を状況ごとに切り替え、UX を最大化する。
- 型安全キー と Suspense でロジック・UI 双方の整合を保つ。
- React Query × tRPC × TypeScript により、高速通信とコンパイル時保証を両立できる。
次回は データフェッチの並列・逐次制御 と型安全フロー設計を扱い、パフォーマンスチューニングをさらに深掘りする。