1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

TSの鬼 第14回:キャッシュ戦略と型保証―React Query × Suspense でデータ整合性を極める

Posted at

はじめに

前回

フロントエンド性能は キャッシュデータ整合性 のバランスで決まる。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 により、高速通信とコンパイル時保証を両立できる。

次回は データフェッチの並列・逐次制御 と型安全フロー設計を扱い、パフォーマンスチューニングをさらに深掘りする。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?