レビュー済み·難易度: 中級·更新: 2026-04-18

TanStack Query で Server Component からデータをプリフェッチする

Server Component で prefetchQuery を実行し HydrationBoundary でクライアントに渡すことで、ウォーターフォールなしに TanStack Query のキャッシュをハイドレートする例。

nextjsapiperformancetanstack-query

対応バージョン

nextjs 15react 19tanstack-query 5

前提環境

TanStack Query の useQuery と Next.js App Router の Server Component を理解していること

概要

Server Component で prefetchQuery を実行し、HydrationBoundary 経由でクライアントコンポーネントに TanStack Query のキャッシュを渡す。初期ロード時のクライアントフェッチ(ウォーターフォール)をなくし、SSR でデータを埋め込んだ状態でハイドレートする。

インストール

npm install @tanstack/react-query

実装

QueryClient のセットアップ(サーバー用)

// lib/getQueryClient.ts
import { QueryClient } from "@tanstack/react-query";
import { cache } from "react";

// Server Component 向けにリクエストごとに新しいインスタンスを生成
export const getQueryClient = cache(() => new QueryClient());

クライアント用 Provider

// app/providers.tsx
"use client";

import { QueryClientProvider, QueryClient } from "@tanstack/react-query";
import { useState } from "react";

export function Providers({ children }: { children: React.ReactNode }) {
  const [queryClient] = useState(() => new QueryClient());
  return (
    <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
  );
}
// app/layout.tsx
import { Providers } from "./providers";

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="ja">
      <body>
        <Providers>{children}</Providers>
      </body>
    </html>
  );
}

データ取得関数

// lib/fetchPosts.ts
export type Post = { id: number; title: string; body: string };

export async function fetchPosts(): Promise<Post[]> {
  const res = await fetch(
    "https://jsonplaceholder.typicode.com/posts?_limit=10"
  );
  if (!res.ok) throw new Error("fetch failed");
  return res.json();
}

export async function fetchPost(id: number): Promise<Post> {
  const res = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`);
  if (!res.ok) throw new Error("fetch failed");
  return res.json();
}

Server Component でプリフェッチ

// app/posts/page.tsx
import {
  dehydrate,
  HydrationBoundary,
} from "@tanstack/react-query";
import { getQueryClient } from "@/lib/getQueryClient";
import { fetchPosts } from "@/lib/fetchPosts";
import { PostList } from "./PostList";

export default async function PostsPage() {
  const queryClient = getQueryClient();

  // サーバー側でキャッシュを先読み
  await queryClient.prefetchQuery({
    queryKey: ["posts"],
    queryFn: fetchPosts,
  });

  return (
    // dehydrate でキャッシュをシリアライズして渡す
    <HydrationBoundary state={dehydrate(queryClient)}>
      <PostList />
    </HydrationBoundary>
  );
}

クライアントコンポーネント(キャッシュがあるため初期フェッチなし)

// app/posts/PostList.tsx
"use client";

import { useQuery } from "@tanstack/react-query";
import { fetchPosts, type Post } from "@/lib/fetchPosts";

export function PostList() {
  const { data: posts, isLoading } = useQuery({
    queryKey: ["posts"],
    queryFn: fetchPosts,
    // HydrationBoundary からキャッシュが渡されるため、
    // 初期レンダリング時は isLoading が false になる
  });

  if (isLoading) return <p>読み込み中...</p>;

  return (
    <main className="mx-auto max-w-xl p-8">
      <h1 className="mb-6 text-2xl font-bold">記事一覧</h1>
      <ul className="space-y-2">
        {posts?.map((post: Post) => (
          <li key={post.id} className="rounded border p-3 text-sm text-gray-700">
            {post.title}
          </li>
        ))}
      </ul>
    </main>
  );
}

ポイント

  • getQueryClientcache() でラップすることで、同一リクエスト内では同じ QueryClient インスタンスを使い回せる(リクエストをまたいで共有されない)
  • prefetchQuery で Server Component 側でデータを取得しキャッシュに入れる。dehydrate でシリアライズして HTML に埋め込む
  • HydrationBoundary がクライアント側でキャッシュを復元(ハイドレート)するため、useQuery の初回レンダリング時にフェッチが走らない
  • 既存の tanstack-query-data-fetching との違い: クライアントのみのフェッチでは初期表示にローディング状態が出るが、プリフェッチパターンでは HTML 時点でデータが含まれる
  • staleTime を設定するとクライアント側での再フェッチタイミングを制御できる(staleTime: 60 * 1000 で 1分間はキャッシュを有効として再フェッチしない)

注意点

既存の tanstack-query-* 4件はすべてクライアントフェッチ。これは Server Component でデータを取得してクライアントにキャッシュを渡すパターン(初期ロード高速化)。

関連サンプル