[先取り] Tanstack Start によるクライアントファーストな RSC のアプローチ
はじめに 🚀
React Server Components(RSC)は、Next.js App Router での採用を機に広く普及しました。一方で、RSC の採用方法については、フレームワークごとに異なる考え方が存在します。
TanStack の作者である Tanner Linsley 氏は、インタビュー[1]にて「RSC をバンドルサイズの削減や静的コンテンツの最適化に役立つツールと見ているが、万能な解決策とは考えていない」と述べています。TanStack Start は、Next.js とは異なるアプローチで RSC をサポートする予定であり、その背景にはクライアントファーストの哲学があります。
本記事では、GitHub の Discussion や Issue、公式ブログなどの一次情報をもとに、以下のテーマを深掘りします。
第1部:RSC の特性と課題 🔍
RSC とは何か - 従来の SSR との違い
React Server Components(RSC)は、React 18 以降で実験的に進められ、React 19 で安定化された新しいパラダイムです。従来の Server-Side Rendering(SSR)と混同されがちですが、根本的に異なる概念です。
従来の SSR の仕組み
従来の SSR では、サーバー上で React コンポーネントを HTML にレンダリングし、クライアントに送信します。その後、クライアント側でハイドレーション(Hydration)を実行します。ハイドレーションとは、サーバーで生成された静的な HTML に対して、イベントハンドラなどをアタッチし、インタラクティブな UI を完成させるプロセスです。
この方式の問題点は、クライアント側で全ての JavaScript を再度実行する必要があることです。大きなライブラリ(Markdown パーサー、シンタックスハイライターなど)を使用している場合、それらすべてがクライアントのバンドルに含まれます。
RSC の仕組み
RSC では、サーバー上で実行されるコンポーネント(Server Component)はクライアントに JavaScript を送信しません。代わりに、RSC Payload(シリアライズされた JSX)という特別なデータ形式で結果だけを送信します。
重要なのは、Server Component で使用したライブラリのコードはクライアントのバンドルに含まれないことです。これにより、バンドルサイズを削減できます。
RSC と SSR の関係
RSC は"静的プリレンダー(ビルド時)" と "リクエスト時のサーバー実行" のどちらでも動作可能です。ビルド時に実行すれば、実行環境にサーバーは不要で静的 HTML として配信できます。一方、SSR と組み合わせてリクエスト時に実行することも可能で、その場合は動的コンテンツの配信や SEO 最適化に活用できます。
RSCは静的配信も可能
元 React 開発者の Dan Abramov 氏のブログ overreacted.io は RSC で構築され、Cloudflare CDN から静的に配信されています。ビルド時に RSC を実行し、静的な HTML を生成することで、サーバーを必要とせず、ホスティングコストはゼロです。
これは「RSC = 常にサーバーが必要」という誤解を覆す好例です。RSC はビルド時実行、静的 HTML 出力、CDN 配信が可能であり、サーバー専用ライブラリ(Markdown パーサーなど)をビルド時に使用しつつ、クライアントバンドルから除外できます。
データコロケーションの課題
Tanner Linsley 氏は、「RSC をバンドルサイズの削減や静的コンテンツの最適化に役立つツールと見ているが、万能な解決策とは考えていない」と述べています[1:1]。
Next.js App Router のような Server Component ファーストのアプローチでは、ブログやマーケティングページなどのコンテンツサイトには最適ですが、高度にインタラクティブなアプリケーションでは課題があります。特にデータコロケーションの面で問題が生じます。
データコロケーションとは、データ要求をそのデータを使う場所に配置するパターンです。コンポーネント内でデータフェッチのロジックを定義することで、コンポーネントが必要とするデータが明確になり、独立性と保守性が高まります。従来のクライアントサイドアプローチでは自然に書けましたが、Server Component ファーストのアプローチではインタラクティブな操作が必要な場合に課題が生じます。
インタラクティブな UI での課題
// ❌ Client Component を使う場合:データの巻き上げが必要
async function Dashboard() {
// 親で全データを並列取得(Promise.all)
const [salesData, tasksData] = await Promise.all([
fetchSalesData(),
fetchTasksData()
])
// Client Component に props で渡す
return (
<>
<SalesWidget data={salesData} /> {/* 'use client' */}
<TaskWidget data={tasksData} /> {/* 'use client' */}
</>
)
}
この「巻き上げ」パターンでは、Promise.all で並列実行は可能ですが、各コンポーネントのデータフェッチロジックを親に移動させる必要があるため、データコロケーションが失われ、コンポーネントの独立性が損なわれます。
Tanner 氏は 2024年6月のツイート[2]で「ダッシュボードウィジェットや生産性アプリのような、コンポーネントレベルでデータの動的性が求められるプロジェクトでは問題になる」と指摘しています。複数の独立したウィジェットがある場合、親コンポーネントが肥大化し、コードが単調でエラーが発生しやすくなります。
Tanner 氏は同じツイートで、Reactが render-as-you-fetch(データの巻き上げ)を推奨することで「データコロケーションという優れた開発体験を犠牲にしている」とさらに踏み込んで批判しています。
SSR がこうした課題と深く関わっており、TanStack Start はデータコロケーションと render-as-you-fetch の両立を目指していることがわかります。
Server Component でのデータコロケーション
インタラクティブな操作が不要な静的コンテンツであれば、Server Component 同士でデータコロケーションを維持できます。
// ✅ 静的コンテンツの場合:データコロケーション維持
async function Dashboard() {
return (
<Suspense fallback={<Loading />}>
{/* 各コンポーネントが独立してデータ管理 */}
<SalesWidget />
<TaskWidget />
</Suspense>
)
}
async function SalesWidget() {
const data = await fetchSalesData()
return <div>{/* 静的な表示のみ */}</div>
}
また、Server Actions を使用することで、Server Component 内でもフォーム送信などのインタラクティブな操作が可能です。
// ✅ Server Component + Server Actions
async function TaskWidget() {
const tasks = await fetchTasksData()
async function updateTask(formData: FormData) {
'use server'
// タスク更新処理
revalidatePath('/dashboard')
}
return (
<div>
{tasks.map(task => (
<form action={updateTask} key={task.id}>
<input type="hidden" name="id" value={task.id} />
<button type="submit">完了</button>
</form>
))}
</div>
)
}
ただし、これが有効なのは以下のような場合に限られます。
- フォーム送信、ボタンクリックによるデータ更新など、サーバーでの処理後にページを再レンダリングする操作
- Progressive Enhancement(JavaScript 無効でも動作)が必要な場合
一方、以下のような高度にインタラクティブな操作が必要な場合、依然として課題があります。
- リアルタイムなクライアント側ステート管理(入力値の即座のバリデーション表示、楽観的更新など)
- ユーザー操作に応じた即座の UI 更新(ドラッグ&ドロップ、リアルタイムフィルタリングなど)
- 複雑なクライアント側のロジック(アニメーション、きめ細かいキャッシュ管理など)
このような場合、コンポーネントを Client Component にする必要があり、結局データの巻き上げ問題に戻ります。Tanner 氏が指摘しているのは、まさにこのような高度にインタラクティブなアプリケーション(ダッシュボードウィジェットや生産性アプリなど)における課題です。
段階的導入の困難さ
RSC のもう一つの大きな課題は、既存のクライアントサイドアプリケーションへの段階的導入が困難であることです[3]。
現在の主流なアプローチは「サーバーファースト」を前提としており、RSC を採用するには App Router のような新しいパラダイムへの全面移行が必要です。管理ポータルや SaaS アプリケーションなど、クライアントサイドで完結している既存アプリにとって、RSC の機能を試すための「簡単な足がかり」は存在しません[3:1]。
React のサーバーレンダリングは当初から良好にサポートされていましたが、常にオプショナルでした。開発者はクライアントから始め、SEO 対策や初期表示の高速化が求められる場面でサーバー機能をオプトインする形で React を使ってきました[3:2]。しかし、RSC は「オールイン」を強いる傾向があり、既存の強力なクライアントサイドアプリがアーキテクチャ全体を書き換えることなく RSC を実験することは非常に困難です[3:3]。
この課題に対し、TanStack Start はクライアントファーストを維持しながら RSC を選択的に導入できる設計を目指しています。
第2部:TanStack Start の RSC 実装 🎯
RSC の本質:プリミティブとしての理解
TanStack Start の RSC 実装を理解する前に、Tanner Linsley 氏が提唱する RSC の本質的な理解を押さえておくことが重要です。
RSC の命名に関する批判
Tanner 氏は、「React Server Components」という名称が不適切であると考えています[4]。「サーバーコンポーネント」という名前は良くないとし、代わりに「プリレンダリングコンポーネント」または「シリアライズ可能コンポーネント」と呼ぶべきだったと述べています。
名称が不適切である理由は、RSC がサーバー上でのみ実行されるわけではないためです。RSC は実際には、クライアントで実行される React とは異なり、クライアントの前に、またはクライアントとは異なる場所で実行されます。例えば以下のような場所でも実行可能です。
- ビルド時のローカルマシン(静的デプロイメント)
- Web Worker
- エッジランタイム
第1部で紹介した Dan Abramov 氏のブログ overreacted.io は、まさにビルド時に RSC を実行し、Cloudflare CDN から静的配信している例です。
RSC は「反復可能な文字列」というプリミティブ
Tanner 氏は、RSC をプリミティブ・ファーストなアプローチで理解することを提案しています[4:1]。
RSC の本質は、React コンポーネントコードを文字列にレンダリングする行為です。より正確には、TypeScript の型で表現すると、どこかに送信され、増分的に読み戻される「反復可能な文字列(iterable string)」、つまりテキストのストリームです。
// RSC の本質的な型(概念的な表現)
type RSC = AsyncIterable<string>
このプリミティブな理解により、RSC を「マジックなブラックボックス」としてではなく、扱いやすいデータ構造として捉えることができます。TanStack Start の RSC 実装は、この考え方を基盤としています。
RSC の実装方法
TanStack Start では、createServerFn から JSX を返すことで RSC を実装します。
公式実装例
以下は Basic RSC Example からの実際のコードです:
公式実装での動作フローは以下の通りです。
-
ルートアクセス: ユーザーがページにアクセス(例:
/posts/1) - Loader 実行: サーバー側で loader が実行され、Server Function を呼び出し
-
Server Function 実行:
createServerFnで定義された handler がサーバー上で実行され、JSX を生成 - RSC ペイロード生成: JSX がシリアライズされた RSC ペイロード(ストリーム)に変換
- クライアント送信: RSC ペイロードがクライアントに送信
-
コンポーネントレンダリング:
useLoaderData()で JSX を取得し、React が通常のコンポーネントと同様に描画
この実装は、loader で Server Function を呼び出し、useLoaderData() で JSX を取得して表示するというシンプルなパターンです。既存の TanStack Router の知識がそのまま活用できます。
独自のアプローチ:制御の反転とスロット
TanStack Start では、一般的な RSC 実装とは異なる独自のアプローチも検討されています。
現状のRSCの課題:「要素」を送信している
Tanner Linsley 氏は、Syntax Podcast[5]で現在の RSC 実装に対する興味深い考察をしています。
現在の一般的な RSC(Next.js など)では、サーバーコンポーネントを実行すると、サーバー側でレンダリングが行われ、その結果である要素(Element)がクライアントに送られます。これは「スナップショット」や「HTML になる直前の JSON」のようなものです。
Tanner 氏はこれに対し、文字通り「コンポーネント(機能を持った部品)」として扱いたいと考えています。サーバーが決めるべきは「データ」と「コアロジック」だけであり、UI の組み立て(レンダリング)の主導権まで必ずしもサーバーが握るべきではない、という考え方です。
解決策:制御の反転(Inversion of Control)
Tanner 氏が強調するキーワードはInversion of Control(IoC:制御の反転)です。
通常、Next.js などで RSC を使うと、サーバーコンポーネントの中にクライアントコンポーネントをインポートして配置します(親がサーバー、子がクライアント)。
// 一般的なRSC(Next.js等)
// ServerComponent.tsx
import ClientCounter from './ClientCounter' // 具体的な実装に依存
export default async function Page() {
const data = await db.query()
// サーバーが「ここでClientCounterを使う」と決め打ち
return <ClientCounter initialCount={data.count} />
}
TanStack Start が目指すアプローチは、「サーバーは枠組みとデータだけを提供し、中身(クライアントの挙動)は外部から注入する(スロット)」というパターンです。
// TanStack Startの目指す形(概念イメージ)
// ServerLayout.tsx
export default async function ServerDataWrapper({ children }) {
const data = await db.query()
// サーバーは「データ」と「子要素の配置場所」だけを提供
// 具体的にどの子要素が来るかは知らなくていい(制御の反転)
return (
<>
<h1>Server Data: {data.title}</h1>
{/* サーバーで取得したデータを、子に流し込む */}
{children(data)}
</>
)
}
このパターンは React の「Render Props」や「Function as Child」に近いですが、RSC の境界線(Network Boundary)で実現しようとしている点が革新的です。
サーバータスクという概念
Tanner 氏は動画内でこう述べています[5:1]。
"I want to be able to send it a task to do this thing on the server... but then come back to me and let me continue to do things on the client"
(サーバーでしかできないタスクをサーバーに送って処理させ、その後戻ってきてクライアントでの処理を継続させたい)
これは、RSC を「UI 生成機」としてではなく、「型安全なデータ処理レイヤー(API 兼コンポーネント)」として捉え直していることを意味します。
TanStack Start はすでに「Server Functions(サーバー関数)」を強力にサポートしています。この Server Functions の考え方を RSC のレンダリング自体にも適用し、「UI の主導権はクライアント(ブラウザ)にあり、データや重い処理だけをサーバーに委譲する」というアーキテクチャを構築しようとしています。
use clientを減らせる理由
Tanner 氏は「これで大多数のユースケースにおいて use client ディレクティブが不要になる」と述べています[5:2]。
Next.js の場合:
インタラクティブな部分を作るたびに、そのファイルを "use client" でマークし、サーバーコンポーネントからインポートします。ファイルが増え、境界線が複雑になります。
TanStack Start のアプローチ:
「サーバーで行うべきタスク(DB アクセス等)」と「クライアントで行うべきタスク(UI 操作)」を分離し、サーバー側はあくまで「サーバーでしかできないタスクを処理する関数」のように振る舞います。
クライアント側から「このサーバー処理(タスク)を実行して、その結果を使って UI を描画してくれ」と依頼する形になれば、コンポーネント自体はクライアント主体のまま、必要な部分だけサーバーの力を借りる(Server Functions / Server Actions の高度な応用)ことができます。
これにより、UI の主導権をクライアントに保ちながら、サーバーの能力を必要な時だけ活用できる、よりSPA(Single Page Application)フレンドリーなアーキテクチャが実現されます。
その他の呼び出しパターン(将来的な可能性)
Tanner Linsley 氏は Discord でのコメント[6]で「from a loader, or useQuery or whatever」「anywhere, not just loaders」と述べており、useQuery での呼び出しや、他の場所からの呼び出しもサポートする方針が示唆されています。
以下は、この発言に基づく将来的な可能性として予想したパターンです。
useQuery で直接呼び出し(将来の可能性)
⚠️ 注意: 重複しますが、このパターンは現時点では公式実装例に含まれていません。将来実装される可能性がある予想例です。
// Server Function の定義
const renderPost = createServerFn({ method: 'GET' })
.inputValidator((postId: string) => postId)
.handler(async ({ data }) => {
const post = await fetchPost(data)
return (
<div className="space-y-2">
<h4 className="text-xl font-bold underline">{post.title}</h4>
<div className="text-sm">{post.body}</div>
</div>
)
})
// コンポーネント内で useQuery を使用
function PostComponent() {
const { postId } = Route.useParams()
// useQuery で Server Function を直接呼び出し
const { data: postJsx, isLoading, isError } = useQuery({
queryKey: ['post', postId],
queryFn: () => renderPost({ data: postId }),
})
if (isLoading) return <div>読み込み中...</div>
if (isError) return <div>エラーが発生しました</div>
return postJsx
}
TanStack Query による RSC 管理の利点
RSC は「サーバー状態」にすぎない
Tanner Linsley 氏は、RSC を特別なものとして扱うのではなく、バックエンドから取得する単なる別の形式のデータ、つまり「サーバー状態にすぎない」と断言しています[7][4:2]。
この視点は非常に重要です。RSC を「streams of serialized JSX として扱うべきで、他の非同期データソースと何ら変わらない」という考え方により、既存の TanStack Query の強力なキャッシュ管理機能をそのまま活用できます。
サーバー状態には、以下のような管理上の課題が伴います。
- 陳腐化(staleness) - データがいつ古くなるか
- 無効化(invalidation) - いつキャッシュを無効にするか
- 依存関係の追跡 - どのデータが関連しているか
- キャッシング - どのようにキャッシュするか
React Server Components をキャッシュする方法について議論している人はまだ多くありませんが、RSC をキャッシュするには、要素自体ではなく、それを反復可能な文字列にシリアライズします[4:3]。
TanStack は、すでにサーバー状態を管理する優れたツール(TanStack Query)を保有しているため、RSC をキャッシュしたり、ディスクに永続化したり、ルーターやクエリを通じて移動させたりする能力をすでに持っています。
TanStack Start での実装アプローチ
TanStack Start では、RSC はサーバー関数または API ルートとしての利用を想定しています[4:4]。
ネットワーク I/O の境界がある場所でJSX または要素を返すと、サーバーコンポーネントが作成されます。クライアントでは、テキストのストリームとして受信され、それを React 要素に変換するためのユーティリティが提供されます。
重要なのは、このストリームは、レンダリングされる前に、ルーターやクエリ、永続化など、任意の場所にパイプすることができ、これによりキャッシングや管理が可能になることです[4:5]。
// 概念的な実装イメージ
const rscStream: AsyncIterable<string> = await serverFn()
// ストリームを様々な場所にパイプ可能
rscStream
|> cache // キャッシュ層
|> persist // 永続化層
|> router // ルーター
|> query // TanStack Query
|> render // 最終的にレンダリング
Next.js では RSC が変更された際に revalidate を実行します。しかし、revalidation といえば TanStack Query の得意分野です。RSC も結局は非同期データであり、非同期状態を意味します。TanStack Query のフロントページにある項目—古いデータ、無効化のタイミング、キャッシュ方法、stale-while-revalidate—はすべて RSC にも当てはまります。
粒度の高い制御と無効化
Next.js 16 では、キャッシング API が改善され、revalidateTag() に cacheLife プロファイルが追加され、updateTag() により即座の更新が可能になりました。しかし、基本的にはタグ単位での revalidation です。
// Next.js 16 の改善例
'use server'
import { updateTag } from 'next/cache'
export async function updateUserProfile(userId: string, profile: Profile) {
await db.users.update(userId, profile)
// 即座に更新(read-your-writes)
updateTag(`user-${userId}`)
}
これは確かに改善ですが、GitHub Discussion #66228 では revalidateTag を使用しても「ページ全体が再レンダリングされる」という報告があり、コンポーネント単位での細かい制御は依然として困難に思います。
Next.js では、新しいサーバーコンポーネントのコンテンツを取得するためにすべてを無効化するというアプローチが採られていますが、RSC を TanStack Query に通すことで、粒度の高い無効化(granular invalidation)を無料で得ることができます[4:6]。
TanStack Start では、TanStack Query を活用することで、より細かい粒度での制御が可能になります(将来的に予定)。
// TanStack Start での将来的な実装イメージ
function Dashboard() {
const queryClient = useQueryClient()
// 各 RSC を独立して管理
const { data: users } = useQuery({
queryKey: ['users'],
queryFn: () => getUsersRSC(),
staleTime: 10 * 60 * 1000, // 10分間は新鮮
})
const { data: invoices } = useQuery({
queryKey: ['invoices'],
queryFn: () => getInvoicesRSC(),
staleTime: 5 * 60 * 1000, // 5分間は新鮮
})
const handleUpdateInvoice = async () => {
await updateInvoice()
// invoices だけ無効化
queryClient.invalidateQueries(['invoices'])
// users は影響を受けず、ページ全体も再レンダリングされない
}
return (
<div>
{users}
{invoices}
</div>
)
}
複数のネストされたルートがある場合、それぞれが同時に並列で RSC をトリガーできます[4:7]。TanStack Query で管理すれば、「この RSC は常に最新でなければならない」「この RSC は 10 分ごとでよい」といった細かい制御が可能になります。
ミューテーションキーも RSC に適用できるようになれば、「この RSC は post ID と user ID を消費する」と指定すれば、それらの変数が変更されると、関連する RSC だけが再実行されます。アプリ全体の React ツリーを再構築する代わりに、invoices に関連する RSC だけが再検証されます。
この方式により、以下が実現されます(将来的に予定)。
- より細かい粒度の制御が可能になり、各 RSC に個別の staleTime や cacheTime を設定できます
- queryKey 単位での選択的な無効化により、他のコンポーネントに影響を与えません
- 複数の RSC を並列実行できます
- データコロケーションを維持し、各コンポーネントが独立してデータを管理できます
その他関連トピック
Isomorphic Loaders について
TanStack Start のIsomorphic Loadersは、サーバーとクライアント両方で動作するデータフェッチの仕組みです。初回 SSR 時はサーバーで実行され、クライアントナビゲーション時はクライアントで実行されます。開発者は環境を意識せず、1つのコードで最適なパフォーマンスが得られます。
Isomorphic Loaders はデータフェッチを担当し、RSCはUI レンダリングを担当する別の技術です。Isomorphic Loaders は RSC なしでも機能するため、TanStack のアプローチは「まず強力なクライアントアプリを構築してから、バンドルサイズ削減が重要な静的コンテンツに対して部分的に RSC を使う」という段階的な導入を想定しています。
将来的な使い分け
RSC サポート後は、アプリケーションの特性に応じて最適なツールを選択できるようになります。
| Isomorphic Loaders | RSC(将来) | |
|---|---|---|
| 返り値 | データ(JSON) | UI(JSX) |
| 最適な用途 | 動的でインタラクティブなデータ TanStack Query の高度なキャッシュ活用 |
静的コンテンツ バンドルサイズの極限削減 |
// ✅ Isomorphic Loaders: データのみ(現在)
export const Route = createFileRoute('/dashboard')({
loader: async () => await fetchDashboardData(),
})
// ✅ RSC: UI も含む(将来)
const DashboardUI = createServerFn()
.handler(async () => {
const data = await fetchDashboardData()
return <div>{data.title}</div> // JSX を返す
})
技術的な課題と進捗
RSC の完全な統合に向けて、いくつかの技術的課題が解決されつつあります。
Streaming SSR の安定化
Issue #3117で報告されているように、Streaming SSR と Suspense の組み合わせで複雑な問題が発生していますが、これらの安定化は将来の RSC 実装に向けた重要な前提条件です。
RSC はこれらの React 18 の機能を活用してサーバー側レンダリングとクライアント側ハイドレーションを実現するため、Streaming SSR と Suspense が正しく動作することが求められます。
// Streaming SSR の例
export const Route = createFileRoute('/stream')({
loader: async () => {
return {
stream: new ReadableStream({
// ストリーミングでデータを送信
})
}
}
})
現在解決中の課題
- Abort Signal とストリーミングの統合(Issue #4651)
- 2025年7月に報告
- クライアントがキャンセルしてもストリーミング Server Functions が継続実行
- レスポンス作成直後に abort listener が削除される
- 影響:メモリリーク、不要な処理の継続
- 状況:未解決
- Middleware のバンドリング最適化(Issue #2783)
- サーバー専用コード(Supabase、ClickHouse クライアント等)がクライアントバンドルに含まれる
-
node:async_hooks等の Node.js 専用モジュールがブラウザ用に externalize される問題 - ツリーシェイキングとコード分離の改善が必要
バンドラーと標準化の課題
RSC の普及には、バンドラーレベルでのサポートと標準化が不可欠です[3:4]。
現在、RSC をサポートしている主要なバンドラーは Next.js の TurboPackのみです。他のエコシステムが RSC を利用できるようになるには、Vite のサポートが鍵となりますが、その進展は遅く、TanStack Start のようなフレームワーク開発にとってボトルネックとなっています[3:5]。
さらに、RSC は Hooks(どこでも使え、どこでも同じように機能する)とは異なり、フレームワークによってサポート度合いやアーキテクチャへの組み込み方が大きく異なります[3:6]。TurboPackのみでの実装に基づいて設計された規約が Vite では機能しないケースもあり、バンドラーレベルでの規約の再検討が必要です[3:7]。
理想的には、サーバーコンポーネントのマニフェスト送信やハイドレーションなどの低レベルのプロトコルがバンドラー間で標準化され、それによりフレームワーク間での標準化も可能になることが望まれています[3:8]。
第3部:設計哲学と Next.js との比較 🔄
TanStack Start の設計哲学
TanStack Start は、Next.js とは根本的に異なる哲学で RSC をサポートする予定です。
クライアントファースト
公式サイトには以下のように明記されています。
"While other frameworks continue to compromise on the client-side application experience we've cultivated as a front-end community over the years, TanStack Start stays true to the client-side first developer experience"
これは クライアントサイド・ファースト + フルスタック というアプローチです。
Tanner Linsley 氏は、複数のインタビュー[8]で TanStack Start について「アプリケーション全体をひっくり返すことなく使える方法で構築している」と述べています。具体的には、全てのコンポーネントを Server Component にする全面的な移行を強制せず、必要な部分だけ RSC を使える段階的な採用を可能にし、TanStack Query や Router の既存メカニズムと調和させるアプローチです。
RSC は「選択肢の一つ」
Tanner 氏は Strapi のインタビュー[8:1]で、RSC 全面採用の Next.js アプローチではなく、「RSC が正しい場合は使い、そうでない場合は使わない」という柔軟性を開発者に与えたいと考えていることを明らかにしています。
また Syntax Podcast[7:1]で、クライアント側のアプローチについて「何年もの間 React の中心であり、ほとんどの場合かなりうまく機能してきた。今でもかなりうまく機能している」と述べ、特定の問題を経験していない限り、このパターンから離れる必要はないと主張しています。
透明性:マジックなブラックボックスではない
TanStack Start は、RSC を「マジックなブラックボックス」として扱うのではなく、シリアライズされた JSX のストリームという本質を開発者に見せる設計を目指しています[8:2]。
TanStack Start では、キャッシングやデータフェッチの動作を明示的に制御できるようにすることで、開発者が予測可能な動作を保証します。キャッシング、トランスポート、レンダリングを細かく調整でき、「なぜこの動作になるのか」が理解しやすい設計を目指しています。
Tanner 氏は Syntax Podcast[7:2]で、API 設計における「マジック」と「コントロール」のバランスについて語っています。TanStack Start は、開発者がキャッシュ、データ転送、レンダリングの各プロセスに対して明示的な制御とカスタマイズ性を持てるよう設計されており、これが「マジックなブラックボックス」を避けるという哲学の根幹にあります。
Next.js との比較
Next.js App Router は RSC を全面的に採用し、デフォルトでサーバーコンポーネントとして動作します。これに対し TanStack Start は、クライアントファーストを維持しながら RSC を段階的に導入できる設計を目指しています。
フレームワーク比較
| 観点 | Next.js App Router | TanStack Start(予定) |
|---|---|---|
| デフォルト | Server Component('use client' でオプトアウト) |
クライアント実行可能なコンポーネント(createServerFn でサーバー機能を呼び出し) |
| RSC 採用 | 全面的(オールイン) | 段階的・選択的 |
| 哲学 | Server Component ファースト | クライアント実行ファースト |
| データフェッチ | コンポーネント内で直接 await 可能 |
createServerFn + loader または useQuery(予想) |
| データコロケーション | インタラクティブな UI では「巻き上げ」が必要 | TanStack Query により各コンポーネントで独立して管理可能 |
主な利点
TanStack Start のアプローチにより、以下が実現されます。
- 段階的な RSC 導入 - アプリケーション全体を RSC に移行する必要がなく、必要な部分だけ選択的に使える
- データコロケーションの維持 - TanStack Query との統合により、各コンポーネントが独立してデータを管理でき、親コンポーネントでデータを取得して子に props で渡す「巻き上げ」パターンが不要
- エコシステムの分断を防ぐ - クライアントファーストの開発体験を維持し、既存のツール(TanStack Query、Router)との調和を保つ
- 開発者の選択肢を増やす - Next.js が RSC 中心、React Router v7 が従来型 SSR を提供する中、TanStack Start はバランス型で柔軟性を重視した新しい選択肢を提供
まとめ 🖌️
React Server Components(RSC)は、バンドルサイズ削減や静的コンテンツ最適化に有効なツールですが、万能ではありません。高度にインタラクティブなアプリケーションでは、従来のクライアント中心のアプローチが適している場合も多くあります。
TanStack Start は、Next.js とは根本的に異なる「クライアントファースト」の哲学で RSC をサポートする予定です。RSC を全面採用するのではなく、開発者が必要な部分でのみ選択的に使えるよう設計されています。これにより、既存のアプリケーションを大きく変更することなく、段階的に RSC の恩恵を受けることが可能になります。
重要なのは、フレームワークが全てを決めるのではなく、開発者が自分のアプリケーションに最適な選択をできる柔軟性です。TanStack Start の RSC 対応は、透明性と制御を重視し、React コミュニティに新しい選択肢を提供するものとなるでしょう。
以上です!
-
Exploring TanStack Ecosystem with Tanner Linsley - Callstack Podcast ↩︎ ↩︎
-
TanStack Start & The Future of React Server Components - Tanner Linsley on Frontend First ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎
-
Server Components Are Not The Future - Tanner Linsley & Theo - t3.gg ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎
-
Fullstack TanStack! The Scoop with Tanner Linsley - Syntax Podcast ↩︎ ↩︎ ↩︎
-
Next Gen Fullstack React with TanStack - Syntax Podcast #833 ↩︎ ↩︎ ↩︎
-
Building Better Apps with TanStack Start and Tanner Linsley - Strapi Blog ↩︎ ↩︎ ↩︎
Discussion