👽

TanStack Start のssr: data-onlyがdata fetchの複雑性を救う

に公開

はじめに

TanStack Start には ssr オプションに3つの値があるのですが、このうちの
data-onlyが素敵な仕様だったので備忘録も兼ねてまとめました。

モード サーバーでの処理 初期HTML
true(デフォルト) loader実行 + HTML生成 コンテンツあり
'data-only' loader実行のみ データのみ埋め込み
false なし(実質CSR) 空

また、'data-only' は、Next.js にはない概念であり、記事ではこのモードが何を解決するのかを比較を交えながら説明します。


まず3つのモードについて

ssr: true(フル SSR)

export const Route = createFileRoute('/pokemon')({
  // ssr: true はデフォルトなので省略可能
  loader: () => fetchPokemon(),
  component: PokemonPage,
})

function PokemonPage() {
  const pokemon = Route.useLoaderData()
  return (
    <ul>
      {pokemon.map((p) => (
        <li key={p.id}>{p.name}</li>
      ))}
    </ul>
  )
}

サーバーでデータ取得 + HTML生成を行い、クライアントはHTMLをそのまま表示してからハイドレーションします。SEOが必要なページの基本形です。(よくあるSSRとしての基本フローです。)


ssr: false(CSR)

export const Route = createFileRoute('/settings')({
  ssr: false,
  loader: () => fetchUserSettings(),
  component: SettingsPage,
})

サーバーは何もしないので、データ取得もレンダリングもすべてクライアントで行います。
(実質CSRです。)


ssr: 'data-only'(本記事のメイン)

export const Route = createFileRoute('/dashboard')({
  ssr: 'data-only',
  loader: () => fetchDashboardData(),
  component: DashboardPage,
})

function DashboardPage() {
  const data = Route.useLoaderData()
  return (
    <div>
      <h1>売上: {data.sales}</h1>
      <Chart data={data.chartData} />
    </div>
  )
}

サーバーでデータだけ取得し、描画はクライアントに任せるモードです。


data-only が解決する問題

3モードの初期表示タイムライン比較

data-only と false の違いは 「データ取得をサーバーで済ませておくかどうか」 です。ネットワークが遅い環境ほど data-only の優位性が出ます。

data-only と true の違いは 「コンポーネントをサーバーでHTMLに変換するかどうか」 です。サーバーの描画コストを削減できます。

つまり data-only は次の2つを両立するモードです。

  • ✅ データはサーバーで取得しておく(false よりも速い)
  • ✅ HTMLの生成はしない(true よりもサーバー負荷が低い)

Next.js との比較

Next.js に data-only に相当する概念はあるか?

結論から言うと、機能としては単体では存在しません。

Next.js の App Router は「Server Components か Client Components か」というコンポーネント単位の設計になっており、「データだけサーバーで取得してレンダリングはクライアントで」というページ単位の制御を1行で表現する方法はなく、近い挙動を実現しようとすると、複数の仕組みを組み合わせる必要があります。

// Next.js App Router で data-only に近い挙動を作る場合

// app/dashboard/page.tsx(Server Component)
export default async function DashboardPage() {
  const data = await fetchDashboardData() // サーバーでデータ取得

  // クライアントコンポーネントにデータを渡す
  return <DashboardClient data={data} />
}

// app/dashboard/DashboardClient.tsx
'use client' // このコンポーネントはクライアントでのみレンダリング

export function DashboardClient({ data }: { data: DashboardData }) {
  return (
    <div>
      <h1>売上: {data.sales}</h1>
      <Chart data={data.chartData} />
    </div>
  )
}

一見シンプルに見えますが、この方法には課題があります。

  • DashboardPage(Server Component)自体はサーバーでHTMLを生成してしまう
  • 完全に「HTMLは生成せず、データだけ渡す」という制御ができない
  • Server / Client の境界をファイルレベルで管理する必要がある

TanStack Start の data-only はどう解決するか

// TanStack Start では1行で意図が明示できる
export const Route = createFileRoute('/dashboard')({
  ssr: 'data-only', // ← これだけ
  loader: () => fetchDashboardData(),
  component: DashboardPage,
})

ルート定義に1行追加するだけで「データはサーバー、描画はクライアント」という戦略が完結します。コンポーネント側は 'use client' など何も意識せず通常の React として書けます。

比較まとめ

TanStack Start data-only Next.js App Router
設定箇所 ルート定義1行 Server/Client Component を分離
コンポーネントの書き方 通常の React 'use client' の管理が必要
HTML生成 しない Server Component 部分はされる
意図の明示性 高い 暗黙的

data-only × TanStack Query の組み合わせ

data-only はそもそもTanstack一門であるTanstack Startの機能なので TanStack Query との相性も抜群です!

export const Route = createFileRoute('/dashboard')({
  ssr: 'data-only',
  loader: ({ context: { queryClient } }) =>
    // サーバーで Query キャッシュを温める
    queryClient.ensureQueryData(dashboardQueryOptions()),
  component: DashboardPage,
})

function DashboardPage() {
  // キャッシュから即取得 → 再 fetch しない
  const { data } = useSuspenseQuery(dashboardQueryOptions())

  return (
    <div>
      <h1>売上: {data.sales}</h1>
      <Chart data={data.chartData} />
    </div>
  )
}

この構成での役割分担は以下のとおりです。

用途 手段
初期データ取得 loader + ensureQueryData(サーバー)
ユーザー操作起因の fetch useQuery(クライアント)
POST / mutation useMutation(クライアント)
ポーリング・リアルタイム更新 useQuery + refetchInterval

サーバーで温めたキャッシュをクライアントがそのまま引き継ぐため、初期表示時の余分な fetch が一切発生しません。


data-only を使うべきケース

data-only が特に有効なケースをまとめると:

  • ログイン必須のページ(クローラーが入れないため SEO 不要)
  • 管理画面・ダッシュボード(インタラクションが多く、サーバーHTMLの恩恵が薄い)
  • 重いクライアント描画を含むページ(チャート・エディタなど、クライアントで動かすもの)
  • サーバー負荷を下げたいが、データウォーターフォールは避けたい

まとめ

ssr: 'data-only' は「データはサーバーで、描画はクライアントで」という戦略をルート定義1行で表現できる、TanStack Start 独自のモードです。

Next.js では Server Component と Client Component の分離で近い挙動を作れますが、ファイルをまたいだ設計が必要になり、最近はやや煩雑になる使い方が散見され公式のベストプラクティスがなにかもあまり腹落ちできておらず難しくなってるなという印象が個人的にあります。

そんな中、TanStack Start はルート単位で戦略を明示できるため、意図がコードに直接現れるのが強みなのでReact系で実装されているプロダクトについては乗り換えることでの利点を享受しつつもベースはReactとなるので移行はしやすくNext.jsが持つ複雑性を緩和できる解決策の一つになると思っています!

早くメジャーバージョン来てほしい

参考

https://tanstack.com/start/latest/docs/framework/react/guide/selective-ssr

Discussion