TanStack QueryによるNext.jsでのシームレスなサーバー状態管理
Ethan Miller
Product Engineer · Leapcell

はじめに
現代のウェブ開発、特にシングルページアプリケーション(SPA)やNext.jsのようなサーバーサイドレンダリング(SSR)フレームワークでは、サーバーサイドデータを効率的に管理することが不可欠です。クライアントサイドの状態管理ライブラリはUIの状態を広範囲にカバーしていますが、非同期サーバーデータのフェッチ、キャッシング、同期、更新といった課題は、しばしば定型的なコード、複雑なロジック、一貫性のないユーザーエクスペリエンスにつながりがちです。データが古い、ローディングインジケーターがない、またはミューテーションが元に戻すのが難しい楽観的更新につながる、といったユーザーの不満を考えてみてください。これらのシナリオは、私たちの従来のアプローチにおける重大なギャップを浮き彫りにします。そこで、TanStack Query(旧React Query)が登場し、これらの複雑さを抽象化し、Next.jsアプリケーションでのデータ管理戦略を向上させるためのエレガントなソリューションを提供します。
コアコンセプトの理解
実践的な実装に入る前に、TanStack Queryの基盤となる基本的な概念を明確に理解しましょう。
サーバー状態
フロントエンドによって所有され、制御されるクライアント状態とは異なり、サーバー状態はリモートサーバー上に存在します。非同期であり、フェッチが必要で、永続化可能(したがって他者によって変更される可能性があり)、しばしば時間とともに「陳腐化」します。例としては、APIから取得するユーザープロフィール、商品リスト、注文詳細などがあります。
キャッシング
TanStack Queryは、サーバーデータに対してインテリジェントで自動的なキャッシングメカニズムを採用しています。データをフェッチすると、それはクエリキャッシュに格納されます。同じデータに対する後続のリクエストは、多くの場合最初にキャッシュにヒットし、瞬時にUIフィードバックを提供し、ネットワークリクエストを削減して、より高速で応答性の高いアプリケーションを実現します。
Stale-While-Revalidate (SWR)
これは強力なキャッシング戦略であり、UIはキャッシュされた(古い)データを即座に表示し、同時にバックグラウンドで最新のデータを再フェッチします。新しいデータが到着すると、キャッシュとUIが更新され、ユーザーは常に何かを迅速に見ることができ、たとえそれが絶対的な最新情報でなくても、最終的には最新の情報を見ることができます。
クエリキー
これらは、TanStack Queryがキャッシュ内の異なるサーバー状態の断片を管理し、区別するために使用する一意の識別子です。通常は配列であり、階層的な構造化と正確な無効化を可能にします。たとえば、['todos']
はすべてのTODOを表し、['todos', todoId]
は特定のTODOを表す場合があります。
クエリ無効化
キャッシュされたデータを「古い」とマークするプロセスであり、TanStack Queryが次にアクセスされたときに再フェッチする必要があることを認識させます。これは、UIが最新のサーバー状態を反映するように、ミューテーション(データの作成、更新、削除など)の後で特に重要です。
楽観的更新
ミューテーションリクエストがサーバーに送信された後、サーバーが変更を確認する前に、UIが即座に更新されるテクニックです。これにより、ユーザーに即時のフィードバックが提供され、アプリケーションがより高速に感じられます。サーバーリクエストが失敗した場合、UIは以前の状態にロールバックできます。
Next.jsにおけるTanStack Query
TanStack Queryはサーバー状態の管理に優れており、データのフェッチ、キャッシング、同期、更新のための宣言的なAPIを提供します。Next.jsと統合すると、特にgetServerSideProps
やgetStaticProps
のようなNext.jsのデータフェッチ機能からの初期データハイドレーションの恩恵を受けつつ、シームレスなデータフローのための堅牢なソリューションを提供します。
コア原則と利点
- 手動キャッシングの排除: 独自のキャッシングロジックの記述に終止符を打ちましょう。TanStack Queryは、未使用データのガベージコレクションを含む、すべてを自動的に処理します。
- データ同期の課題解決: ミューテーション、ウィンドウフォーカス、ネットワーク再接続の後でも、UIがバックエンドデータと同期していることを保証します。
- 定型コードの削減:
useQuery
やuseMutation
のようなフックによるシンプルなデータフェッチは、ローディング状態、エラーハンドリング、データフェッチのための繰り返しコードを大幅に削減します。 - 優れた開発者エクスペリエンス: 包括的な開発者ツールは、クエリキャッシュへの洞察を提供し、デバッグとデータフローの理解を容易にします。
- パフォーマンスの向上: 積極的なキャッシングとバックグラウンドでの再フェッチは、よりスピーディなユーザーエクスペリエンスを提供します。
実装例
Next.jsアプリケーションにTanStack Queryを統合する方法を説明しましょう。
まず、必要なパッケージをインストールします。
npm install @tanstack/react-query @tanstack/react-query-devtools # または yarn add @tanstack/react-query @tanstack/react-query-devtools
次に、アプリケーション全体でQueryClient
を利用できるように、_app.tsx
でQueryClientProvider
を設定します。
// pages/_app.tsx import type { AppProps } from 'next/app'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; import { useState } from 'react'; function MyApp({ Component, pageProps }: AppProps) { const [queryClient] = useState(() => new QueryClient()); return ( <QueryClientProvider client={queryClient}> <Component {...pageProps} /> <ReactQueryDevtools initialIsOpen={false} /> </QueryClientProvider> ); } export default MyApp;
次に、TODOリストを取得して表示するシンプルなコンポーネントを作成します。
// components/TodoList.tsx import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; interface Todo { id: number; title: string; completed: boolean; } // API呼び出しを模倣 const fetchTodos = async (): Promise<Todo[]> => { const res = await fetch('/api/todos'); // APIルートがあると仮定 if (!res.ok) { throw new Error('Failed to fetch todos'); } return res.json(); }; const addTodo = async (todoTitle: string): Promise<Todo> => { const res = await fetch('/api/todos', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ title: todoTitle, completed: false }), }); if (!res.ok) { throw new Error('Failed to add todo'); } return res.json(); }; const TodoList: React.FC = () => { const queryClient = useQueryClient(); const { data: todos, isLoading, isError, error } = useQuery<Todo[], Error>({ queryKey: ['todos'], queryFn: fetchTodos, }); const { mutate: addATodo, isLoading: isAddingTodo } = useMutation< Todo, Error, string >({ mutationFn: addTodo, onSuccess: () => { // 追加が成功した後、リストを再フェッチするために 'todos' クエリを無効化します queryClient.invalidateQueries({ queryKey: ['todos'] }); }, }); if (isLoading) return <div>Loading todos...</div>; if (isError) return <div>Error: {error?.message}</div>; const handleAddTodo = () => { const newTodoTitle = prompt('Enter new todo title:'); if (newTodoTitle) { addATodo(newTodoTitle); } }; return ( <div> <h1>Todo List</h1> <ul> {todos?.map((todo) => ( <li key={todo.id}> {todo.title} - {todo.completed ? 'Completed' : 'Pending'} </li> ))} </ul> <button onClick={handleAddTodo} disabled={isAddingTodo}> {isAddingTodo ? 'Adding...' : 'Add Todo'} </button> </div> ); }; export default TodoList;
そして、Next.js APIルート(例: pages/api/todos.ts
):
// pages/api/todos.ts import { NextApiRequest, NextApiResponse } from 'next'; let todos = [ { id: 1, title: 'Learn Next.js', completed: false }, { id: 2, title: 'Explore TanStack Query', completed: false }, ]; let nextId = 3; export default function handler(req: NextApiRequest, res: NextApiResponse) { if (req.method === 'GET') { res.status(200).json(todos); } else if (req.method === 'POST') { const { title, completed } = req.body; const newTodo = { id: nextId++, title, completed }; todos.push(newTodo); res.status(201).json(newTodo); } else { res.setHeader('Allow', ['GET', 'POST']); res.status(405).end(`Method ${req.method} Not Allowed`); } }
この例では:
useQuery
は、fetchTodos
関数と['todos']
を一意のキーとして使用して、TODOリストを取得します。これは、data
、isLoading
、isError
、error
の状態をそのまま提供します。useMutation
はaddTodo
操作に使用されます。成功時 (onSuccess
) にqueryClient.invalidateQueries
がqueryKey: ['todos']
で呼び出されます。これにより、TanStack Queryはtodos
データが古くなったと認識し、次にアクセスされたときに再フェッチする必要があることを示します。これにより、リストが常に最新のサーバー状態を反映することが保証されます。
高度なパターン: Next.jsとの初期データハイドレーション
SEOとより高速な初期ページロードのために、Next.jsはしばしばサーバーサイドレンダリング(SSR)または静的サイト生成(SSG)を使用します。TanStack Queryは、サーバーでフェッチされたデータでクライアントサイドキャッシュをハイドレートできます。
// pages/index.tsx import { dehydrate, QueryClient, Hydrate } from '@tanstack/react-query'; import { GetServerSideProps } from 'next'; import TodoList from '../components/TodoList'; interface Todo { id: number; title: string; completed: boolean; } const fetchTodos = async (): Promise<Todo[]> => { const res = await fetch('http://localhost:3000/api/todos'); // サーバーでは絶対URLを使用 if (!res.ok) { throw new Error('Failed to fetch todos'); } return res.json(); }; export const getServerSideProps: GetServerSideProps = async () => { const queryClient = new QueryClient(); // サーバーでデータをプリフェッチ await queryClient.prefetchQuery({ queryKey: ['todos'], queryFn: fetchTodos, }); return { props: { dehydratedState: dehydrate(queryClient), }, }; }; export default function HomePage({ dehydratedState }: { dehydratedState: unknown }) { return ( <Hydrate state={dehydratedState}> <TodoList /> </Hydrate> ); }
ここで、getServerSideProps
はサーバーで todos をフェッチし、dehydratedState
を作成してクライアントに渡します。Hydrate
コンポーネントは、このデータをクライアントサイドのTanStack Queryキャッシュに注入するため、TodoList
は初期レンダリング時にデータを再フェッチする必要がなくなります。これにより、SSRの利点(高速な初期レンダリング)とTanStack Queryの強力なクライアントサイドキャッシングおよび同期を組み合わせることができます。
結論
TanStack Queryは、Reactアプリケーション、特に複雑なNext.jsプロジェクトにおけるサーバー状態の扱い方を根本的に変えます。フェッチ、キャッシング、データ同期を自動化することで、定型コードを大幅に削減し、パフォーマンスを向上させ、開発者とユーザーのエクスペリエンスを向上させます。クエリ無効化や楽観的更新のような強力な機能と組み合わせた直感的なAPIは、堅牢でリアクティブなウェブアプリケーションを構築するための不可欠なツールとなります。TanStack Queryを採用して、アプリケーションのデータ管理に新たなレベルの効率性とエレガンスを解き放ちましょう。データフェッチを課題から解決済みの問題へと変えます。