Reactにおける実践的なサーバー状態管理:TanStack Queryによるキャッシュと同期

Programming tutorial - IT technology blog
Programming tutorial - IT technology blog

手動フェッチの混乱を超えて

2018年当時、私はすべてのAPIコールに対して, 同じようなロジックを何週間もかけて書いていました。フェッチのたびに、data、loading、errorという3つの状態を初期化していたのです。コンポーネントはuseEffectフックで膨れ上がり、デバッグは悪夢のようでした。useStateuseContextだけでコンポーネント間のデータ同期を試みたことがある人なら、アプリが成長した瞬間にそのアーキテクチャが崩壊することを知っているはずです。

私たちが陥りやすいミスは、サーバー状態(Server State)クライアント状態(Client State)と同じように扱ってしまうことです. クライアント状態はローカルなものであり、サイドバーの開閉やテキスト入力のように完全に制御可能です。一方、サーバー状態はリモートにあります。それは自分が所有しているものではなく、数ミリ秒で古くなる可能性のある「スナップショット」に過ぎません。Reduxのような従来のツールでは、このライフサイクルを手動で管理せざるを得ず、キャッシュ、無効化、再取得のためのコードを何度も書くことになります。

TanStack Query(旧React Query)は、非同期状態マネージャーとして機能します。キャッシュを処理し、読み込み状態を自動的に管理します。これにより、何百行ものボイラープレートを書くことなく、UIとバックエンドの同期を保つことができます。APIコールを、煩雑なサイドエフェクトとしてではなく、宣言的な依存関係として扱うことができるようになります。

セットアップ:ライブラリの準備

まず、プロジェクトにライブラリを導入しましょう。TanStack Queryはフレームワークに依存しませんが、ここではReactパッケージに焦点を当てます。また、DevToolsのインストールを強くお勧めします。キャッシュに何が保持されているかを瞬時に確認できるため、開発において非常に重宝します。

# npmを使用する場合
npm install @tanstack/react-query @tanstack/react-query-devtools

# yarnを使用する場合
yarn add @tanstack/react-query @tanstack/react-query-devtools

# pnpmを使用する場合
pnpm add @tanstack/react-query @tanstack/react-query-devtools

インストールしたら、アプリケーションをQueryClientProviderでラップします。これにより、ツリー内のすべてのコンポーネントがクエリキャッシュと通信できるようになります。

import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';

const queryClient = new QueryClient();

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      {/* アプリのコンポーネント */}
      <ReactQueryDevtools initialIsOpen={false} />
    </QueryClientProvider>
  );
}

実装:よりクリーンで高速なデータ処理

セットアップが完了したら、useQueryフックを使用できます。ここからが本番です。最近のプロジェクトでは、20以上のAPIエンドポイントがありましたが、このパターンに切り替えたことで、約1,200行の冗長な状態ロジックを削除することができました。

useQueryによるデータ取得

プロジェクトリストの実装例を見てみましょう。データ取得のためにuseEffectを排除することで、コンポーネントがいかにクリーンに保たれているかに注目してください。

import { useQuery } from '@tanstack/react-query';

const fetchProjects = async () => {
  const response = await fetch('/api/projects');
  if (!response.ok) throw new Error('ネットワークレスポンスが正常ではありませんでした');
  return response.json();
};

function ProjectList() {
  const { data, isLoading, isError, error } = useQuery({ 
    queryKey: ['projects'],
    queryFn: fetchProjects,
    staleTime: 1000 * 60 * 5, // 5分間
  });

  if (isLoading) return <div>プロジェクトを読み込み中...</div>;
  if (isError) return <div>エラー: {error.message}</div>;

  return (
    <ul>
      {data.map((project) => (
        <li key={project.id}>{project.name}</li>
      ))}
    </ul>
  );
}

よくある間違い:staleTimeとgcTime

最もよく見かけるミスは、staleTimeを無視することです。デフォルトでは0に設定されています。つまり、ユーザーがタブを切り替えて戻ってくるたびに、バックグラウンドでのフェッチがトリガーされます。最新のデータがあるのは良いことですが、不必要にサーバーに負荷をかける可能性があります。

  • staleTime: データが「鮮度(fresh)」を保つ期間。この期間内であれば、ネットワークリクエストを行わずにキャッシュからデータが返されます。
  • gcTime (旧 cacheTime): 非アクティブなデータがガベージコレクションされるまでメモリに残る期間。

月間ユーザー数5万人の本番アプリでは、グローバルなstaleTimeを60秒に設定しました。この単純な変更だけで、ユーザー体験を損なうことなくサーバー負荷を45%削減できました。

ミューテーションと無効化のハンドリング

データの取得は仕事の半分に過ぎません。データを送り返す必要もあります。useMutationフックはこれを完璧に処理します。コツはクエリの無効化(Query Invalidation)です。プロジェクトを追加した後、リストを自動的に更新させたい場合に有効です。

import { useMutation, useQueryClient } from '@tanstack/react-query';

function AddProjectForm() {
  const queryClient = useQueryClient();

  const mutation = useMutation({
    mutationFn: (newProject) => {
      return fetch('/api/projects', {
        method: 'POST',
        body: JSON.stringify(newProject),
      });
    },
    onSuccess: () => {
      // 'projects'を即座に古い(stale)とマークし、再取得をトリガーします
      queryClient.invalidateQueries({ queryKey: ['projects'] });
    },
  });

  const handleSubmit = (event) => {
    event.preventDefault();
    mutation.mutate({ name: '素晴らしい新プロジェクト' });
  };

  return (
    <form onSubmit={handleSubmit}>
      <button type="submit" disabled={mutation.isPending}>
        {mutation.isPending ? '保存中...' : 'プロジェクトを追加'}
      </button>
    </form>
  );
}

プロのチェックポイント:監視と信頼性

クエリの実装が終わったら、DevToolsを使用して挙動を確認しましょう。キャッシュが裏側で予期しない動きをしていないか、100%確信を持つための唯一の方法です。

以下の4つのチェックポイントを意識してください:

  1. 階層的なキー: 単純な文字列ではなく ['projects', id] を使用します。これにより、キャッシュの他の部分に影響を与えず、特定のデータのみを無効化の対象にできます。
  2. リトライロジック: TanStack Queryは、デフォルトで失敗したリクエストを3回リトライします。重要なUI要素では、ユーザーが10秒間もローディング画面を見続けなくて済むよう、リトライ回数を1回に減らすことも検討してください。
  3. ウィンドウのフォーカス: ブラウザのタブを切り替える際にDevToolsを観察してください。データが静的なものであれば、refetchOnWindowFocusを無効にして帯域幅を節約しましょう。
  4. Error Boundaries: throwOnErrorオプションを使用して、コンポーネントツリーの上位でAPIエラーをキャッチします。これにより、個々のコンポーネントをよりシンプルに保てます。

サーバー状態の管理は、頭の痛い問題であるべきではありません。重労働をTanStack Queryに任せることで、レースコンディションとの戦いをやめ、機能の開発に集中できるようになります。このアーキテクチャへの転換は、より堅牢なアプリと、より幸福な開発チームを生み出すでしょう。

Share: