TanStack Start/Router から来た人へ
このページは、TanStack Router、TanStack Start、または TanStack のクライアント中心の設計に慣れている人が、@lazarv/react-server の考え方を理解するための比較です。移行手順書ではありません。どの概念が似ていて、どこが本質的に違い、TanStack の機能を @lazarv/react-server ではどう捉えるかを説明します。
TanStack 側の前提は現在の TanStack Start と TanStack Router のドキュメントです。特に TanStack Start、ルーティング、実行モデル、コード実行パターン、サーバー関数、サーバールート、ミドルウェア、選択的 SSR、静的事前レンダリング、Router overview、データ読み込み、path params、検索パラメータ、ルーティング概念、document head 管理 を比較対象にしています。TanStack Start の React docs では、RSC はまだ実験的で opt-in として扱われています。
TanStack Router は、型の強いクライアントルーターです。ファイルベース/コードベースのルート、ローダー、ルートキャッシュ、検索パラメータ検証、プリロード、ナビゲーション API、devtools を中心にアプリを組み立てます。TanStack Start はそこに SSR、ストリーミング、サーバー関数、サーバールート、ミドルウェア、ホスティング統合、静的事前レンダリングを追加します。
@lazarv/react-server は別の中心から始まります。これは Vite 上に構築された React Server Components ランタイムとサーバーです。ルーティング、サーバー関数、HTTP ヘルパー、キャッシュ、静的出力、ミドルウェア、ワーカー、ライブコンポーネント、デプロイアダプターは、その RSC の中核を支える機能です。
もっとも大きな違いは実行モデルです。
- TanStack Start では、通常のルートコードとローダーは既定で同型です。SSR 中はサーバーで動き、クライアント遷移中はブラウザーでも動きます。サーバー専用処理は
createServerFn、createServerOnlyFn、サーバールート、保護されたファイルの背後に移します。 @lazarv/react-serverでは、ルートコンポーネントは既定でサーバーコンポーネントです。"use client"境界、クライアント専用ルート、クライアントリソースへ明示的に渡さない限り、サーバーコードはサーバーに留まります。
TanStack 風のアプリにとって重要な橋渡しもあります。アプリのルート自体が "use client" モジュールの場合、@lazarv/react-server はクライアントルート SSR の短縮経路を使います。ページは React DOM SSR で描画されますが、リクエストスコープの "use cache: request" の結果は RSC シリアライザーでシリアライズされ、対象を絞ったハイドレーションデータとして注入されます。つまりこれは「クライアントルートの通常 SSR」と「必要なキャッシュエントリだけの RSC 形式ハイドレーションデータ」であり、ページ全体の RSC ペイロードではありません。
| 領域 | TanStack Start/Router | @lazarv/react-server |
|---|---|---|
| 全体像 | Router 中心。Start ではフルスタック機能を追加 | RSC ランタイムと本番サーバー |
| ビルド | Vite と TanStack プラグイン | Vite Environment API と @lazarv/react-server プラグイン |
| UI モデル | SSR 付きの同型 React アプリ。RSC は実験的 opt-in | RSC が既定のレンダリングモデル |
| ルーティング | createFileRoute、createRouter、routeTree.gen | ファイルルーターとコードベースの型付きルーター |
| データ読み込み | beforeLoad、ローダー、ルートキャッシュ、TanStack Query、サーバー関数 | 非同期サーバーコンポーネント、リソース、"use cache"、レスポンスキャッシュ |
| 検索パラメータ | JSON を扱える型付き/検証済み検索パラメータ | 型付き/検証済み検索パラメータ、関数 updater、SearchParams 変換 |
| 更新/RPC | createServerFn | "use server" サーバー関数 |
| API | route server.handlers / サーバールート | *.server.* と HTTP メソッド接頭辞ファイル |
| ミドルウェア | リクエストミドルウェアとサーバー関数ミドルウェア | ルートセグメントごとの *.middleware.* |
| 描画モード | SSR、SPA mode、選択的 SSR、事前レンダリング、ISR 風運用 | RSC SSR、クライアント専用ルート、PPR、静的出力 |
| デプロイ | Start のホスティング設定 | Node、Bun、Deno、Vercel、Netlify、Cloudflare、AWS、Azure、Firebase、Docker、static export |
| TanStack の概念 | @lazarv/react-server の考え方 | 重要な違い |
|---|---|---|
src/router.tsx と createRouter({ routeTree }) | ファイルルーターの自動エントリ、または createRouter | routeTree.gen は不要です。 |
src/routes/__root.tsx | layout.tsx | どちらもアプリの外枠ですが、@lazarv/react-server のレイアウトは既定でサーバーコンポーネントです。 |
createFileRoute('/posts')({ component }) | ページファイル、または createRoute('/posts', <Page />) | TanStack の Route object を export する必要はありません。 |
$postId | [postId] | ファイルルーターでは bracket segment を使います。 |
validateSearch | validate.search、型付き検索、SearchParams 変換 | TanStack の JSON 検索モデルの完全互換ではありません。 |
beforeLoad | ミドルウェア、レイアウト/ページ内処理、リソース、ルート検証 | 一対一対応のライフサイクルフックはありません。 |
loader | 非同期サーバーコンポーネント、リソース、サーバー関数 | データ読み込みの中心はルートローダーではありません。 |
Route.useLoaderData() | props、リソース .use()、ローカルの async データ | データはローダー結果 object ではなく、コンポーネントツリーに置くことが多いです。 |
| ルーターキャッシュ | "use cache"、リソース、レスポンスキャッシュ | キャッシュはルートローダー中心ではなく、ディレクティブ/プロバイダー中心です。 |
router.invalidate() | invalidate、revalidate、リソース無効化、refresh/遷移 | 関数、タグ、リソース、レスポンスを対象にします。 |
<Link to="/posts" search={...} /> | Link または型付き route.Link | どちらも型付き遷移を扱えます。@lazarv/react-server はリソースのプリフェッチもできます。 |
RouterProvider | ランタイム内部のクライアントプロバイダー | 通常のファイルルーターアプリではアプリ側に置きません。 |
| TanStack のクライアントルートアプリ | "use client" ルートとクライアントルート SSR | SSR しつつ、リクエストキャッシュ値だけを RSC 形式でハイドレートできます。 |
| TanStack Router devtools | --devtools | TanStack はルート状態を見るのに対し、@lazarv/react-server は RSC ペイロード、キャッシュ、ルート、アウトレット、ワーカー、ライブコンポーネント、ログを見ます。 |
createServerFn | "use server" async 関数 | builder API ではなく React 方式のディレクティブです。 |
ClientOnly / useHydrated | ClientOnly、"use client"、"use hydrate" | サーバーで描画した部分を後からハイドレートするアイランドも使えます。 |
route head、HeadContent、Scripts | 通常の React <head> とレイアウト | TanStack の head API は使いません。 |
| TanStack Query | クライアントコンポーネント内の TanStack Query、または @lazarv/react-server リソース | Query は使えますが、サーバー由来データは RSC/リソースに寄せることが多いです。 |
TanStack Router はアプリケーションルーターです。ルート照合、パラメータ、検索パラメータ、ローダー、ルートキャッシュ、ルートコンテキスト、プリロード、リダイレクト、エラー、ナビゲーションを調整します。TanStack Start はそこに、ドキュメント全体の SSR、ストリーミング、サーバー関数、サーバールート、ミドルウェア、環境処理、デプロイ処理を追加します。
@lazarv/react-server は TanStack Router host ではなく、@tanstack/react-router や @tanstack/react-start の API を実装しません。中心にあるのはサーバーコンポーネントツリーです。ルーティングは重要ですが、データ取得とサーバー実行を調整する唯一の場所ではありません。
ランタイムは React を pin して RSC wire protocol の互換性を保ち、Vite をビルド/ツールの拡張ポイントとして使います。本番サーバーには health/readiness、keep-alive/timeout、graceful shutdown、body/multipart limits、adaptive backpressure、CSRF origin 検証、OpenTelemetry などの運用機能があります。
TanStack Start は既定で同型です。ルートローダーは本質的にサーバー専用ではありません。SSR 中はサーバーで、クライアント遷移中はブラウザーで実行されます。そのため、Start の docs では createServerFn、createServerOnlyFn、createClientOnlyFn、createIsomorphicFn、.server.* / .client.* ファイル、import 保護が重要になります。
@lazarv/react-server は既定でサーバー優先です。
posts.page.tsxexport default async function PostsPage() {
const posts = await db.posts.findMany();
return <PostList posts={posts} />;
}
この DB 呼び出しを、クライアントから隠すためだけに RPC で包む必要はありません。すでにサーバーコンポーネントの中にあります。ブラウザー state、effects、イベントハンドラー、ブラウザー API が必要な場所だけ "use client" 境界を置きます。
like-button.tsx"use client";
export default function LikeButton() {
const [liked, setLiked] = useState(false);
return <button onClick={() => setLiked((value) => !value)}>Like</button>;
}
TanStack Start/Router では、ルートツリーをクライアントが所有し、初回 HTML だけサーバーで作る、という設計がよくあります。@lazarv/react-server もこの形をサポートします。
アプリのエントリが "use client" モジュールの場合、ランタイムはクライアントルート用の SSR 経路に切り替えます。
src/index.jsx"use client";
import App from "./App.jsx";
export default function Root() {
return <App />;
}
このルートは React DOM SSR で直接描画されます。ページルート用にサーバーコンポーネントツリーをエンコードする必要がないため、ページ全体の RSC Flight パイプラインは使いません。ブラウザーは、サーバーで React DOM が描画した同じクライアントツリーをハイドレートします。
それでもリクエストスコープのキャッシュ値はハイドレーションできます。
App.jsx"use client";
import { use } from "react";
import { getRequestData } from "./get-request-data.mjs";
export default function App() {
const data = use(getRequestData());
return <pre>{JSON.stringify(data, null, 2)}</pre>;
}
get-request-data.mjslet calls = 0;
export async function getRequestData() {
"use cache: request";
calls += 1;
return {
calls,
renderedAt: new Date(),
};
}
SSR 中に getRequestData() は現在の HTTP リクエストで1回だけ実行されます。解決された値はリクエストキャッシュに入り、HTML ストリームの flush 時に RSC シリアライザーでシリアライズされ、self.__react_server_request_cache_entries__ に注入されます。ブラウザー側のキャッシュラッパーはハイドレーション中にそれを同期的に読むため、use(getRequestData()) は初回描画で値を得られます。
この仕組みは「対象を絞った RSC 形式のハイドレーションデータを持つ SSR」です。
- ページ全体は RSC Flight ペイロードではありません。
- ページルートは通常の React DOM SSR と React DOM hydration のままです。
- ハイドレーションで必要なリクエストキャッシュエントリだけを RSC 形式で送ります。
- Suspense 境界が解決するたびに、その値を HTML へ流せます。
"use cache: request; no-hydrate"またはhydrate=falseの値はサーバー/リクエスト内に留まり、ブラウザーへ出ません。
これは、TanStack 風のクライアントシェルを保ちながら SSR HTML を得たい場合に有効です。ただしルートが "use client" なので、推移的に import したコードはクライアントバンドル候補になります。secret を読むコードや DB 専用コードをこのルートに import しないでください。サーバー描画中心のページでは、通常の RSC ルートを優先します。
TanStack Start の典型的な構成は src/router.tsx、routeTree.gen.ts、src/routes です。
TanStack Startsrc/ |-- router.tsx |-- routeTree.gen.ts `-- routes/ |-- __root.tsx |-- index.tsx |-- about.tsx `-- posts/ `-- $postId.tsx
@lazarv/react-server のファイルルーターは、CLI に明示的なエントリを渡さないと有効になります。既定のルートは src/pages です。
@lazarv/react-serversrc/pages/ |-- layout.tsx |-- page.tsx |-- about.tsx `-- posts/ `-- [postId].page.tsx
型付きファイルルーターは仮想 @lazarv/react-server/routes モジュールを生成します。各ルート記述子は .Link、.href()、.useParams()、.useSearchParams()、.createPage()、.createLayout()、.createLoading()、.createError()、.createMiddleware() などを持ちます。
コードベースのルーティングもできます。
router.tsximport { createRoute, createRouter } from "@lazarv/react-server/router";
import { z } from "zod";
export const post = createRoute("/posts/[postId]", {
exact: true,
validate: {
params: z.object({ postId: z.string().min(1) }),
},
});
export const router = createRouter({
post: createRoute(post, <PostPage />),
});
考え方は似ていますが、API は互換ではありません。
TanStack Router は URL 状態に強く、検索パラメータを JSON 互換の構造として扱えます。@lazarv/react-server でも検索パラメータを検証し、型付きフックとリンクで扱えますが、TanStack Router の JSON 検索モデルをそのまま再現するものではありません。
products.page.tsximport { products } from "@lazarv/react-server/routes";
import { z } from "zod";
export const validate = {
search: z.object({
page: z.coerce.number().int().positive().catch(1),
sort: z.enum(["name", "price"]).catch("name"),
}),
};
export default products.createPage(() => {
const search = products.useSearchParams();
return (
<products.Link search={(prev) => ({ ...prev, page: search.page + 1 })}>
Next page
</products.Link>
);
});
データ読み込みの中心も違います。TanStack Router では beforeLoad、ローダー、ルーターキャッシュがページデータを調整します。@lazarv/react-server では、サーバーコンポーネントツリーとリソースが中心です。
posts.page.tsxexport default async function PostsPage() {
const posts = await db.posts.findMany();
return <PostList posts={posts} />;
}
resources/posts.tsimport { createResource } from "@lazarv/react-server/resources";
import { z } from "zod";
export const posts = createResource({
key: z.object({
page: z.coerce.number().int().positive().default(1),
}),
}).bind(async ({ page }) => {
"use cache; tags=posts";
return db.posts.page(page);
});
キャッシュもルートローダーの結果だけに結びつきません。"use cache"、TTL、タグ、プロファイル、キャッシュプロバイダー、invalidate、revalidate、withCache、useResponseCache を使い、ページ、レイアウト、リソース、サーバー関数、API ルートから同じキャッシュ機構を使えます。
TanStack Start の createServerFn() は builder API です。@lazarv/react-server では async 関数に "use server" を付けます。
todos.page.tsximport { invalidate, redirect } from "@lazarv/react-server";
async function getTodos() {
"use cache; tags=todos";
return db.todos.findMany();
}
async function createTodo(formData: FormData) {
"use server";
await db.todos.create({ text: String(formData.get("text") ?? "") });
await invalidate(getTodos);
redirect("/todos");
}
export default async function TodosPage() {
const todos = await getTodos();
return (
<form action={createTodo}>
<ul>{todos.map((todo) => <li key={todo.id}>{todo.text}</li>)}</ul>
<input name="text" />
<button type="submit">Create</button>
</form>
);
}
サーバー関数の参照は既定で暗号化されます。必要に応じてキーのローテーション、CSRF origin 検証、ペイロード/デコード制限、構造的防御を使えます。
TanStack Start の RSC は実験的で、renderServerComponent や createCompositeComponent から生成した値をローダー経由でクライアントに渡す形として説明されています。@lazarv/react-server では RSC が既定のアプリモデルです。サーバー側のデータ取得とクライアント側の対話性は、React のサーバー/クライアント境界で合成します。
クライアントナビゲーションも RSC を意識しています。サーバールートへの遷移では次の RSC ペイロードを取得し、"use client" ページへの遷移ではブラウザーだけで進めます。これによりクライアント専用ルート間のローカル state を保持できます。
TanStack Start の server routes は route 定義の server.handlers に置きます。@lazarv/react-server ではサーバールートファイルを使います。
GET.hello.server.mjsexport default async function hello() {
return new Response("Hello");
}
ミドルウェアも builder ではなく、ルートツリーに沿ったファイルです。
index.middleware.mjsimport { redirect, usePathname } from "@lazarv/react-server";
export default async function adminMiddleware() {
const pathname = usePathname();
if (pathname.startsWith("/admin") && !(await isSignedIn())) {
redirect("/login");
}
}
HTTP ヘルパーはサーバーコンポーネント、ミドルウェア、API ルート、サーバー関数で共通して使えます。
TanStack Start には SSR、SPA mode、選択的 SSR、静的事前レンダリング、ISR 風の運用、実験的 RSC 合成があります。@lazarv/react-server では、目的ごとにプリミティブを選びます。
| 目的 | TanStack Start/Router | @lazarv/react-server |
|---|---|---|
| 初期 HTML をサーバーで作る | SSR | RSC ツリーからのストリーミング SSR |
| クライアント所有ルートツリーを SSR する | Start/Router のアプリシェル SSR | "use client" ルートとリクエストキャッシュのハイドレーションデータ |
| そのルートでサーバーを使わない | SPA mode / 選択的 SSR | "use client" クライアント専用ページ |
| 静的出力 | 静的事前レンダリング | .static.* と export() 設定 |
| 再検証 | HTTP キャッシュ、ルーターキャッシュ、Query キャッシュ | "use cache"、invalidate、revalidate |
| 部分的な server/client 分割 | ClientOnly、選択的 SSR、実験的 RSC | サーバーコンポーネント、クライアントコンポーネント、"use hydrate" |
| 静的出力の動的部分 | 選択的 SSR / ISR 風運用 | "use dynamic" と "use static" による PPR |
TanStack の head、HeadContent、Scripts に相当する専用 API は使いません。通常の React markup で <head> を書きます。
layout.tsxexport default function RootLayout({ children }) {
return (
<html>
<head>
<title>My App</title>
<meta name="description" content="My app" />
</head>
<body>{children}</body>
</html>
);
}
設定は react-server.config.* と vite.config.* に分けます。react-server.config.* はランタイム、ルーター、HTTP サーバー、キャッシュ、計装、アダプター、静的出力などを扱います。Vite 側の設定は通常通り Vite に置きます。環境変数については、サーバーコンポーネントとサーバー関数では secret を読めますが、secret を読むモジュールをクライアント側へ import しないようにします。
TanStack Start では、認証はルートガード、beforeLoad、サーバー関数、サーバールート、cookie、リクエストミドルウェア、サーバー関数ミドルウェアで扱うことが多いです。@lazarv/react-server でも原則は同じです。UI ルートはミドルウェアやサーバー側ロジックで守り、データアクセスはサーバー関数/API ルート内でも必ず検証します。
ランタイム境界では、暗号化されたサーバー関数参照、キーのローテーション、CSRF origin 検証、body/multipart limits、デコード制限、prototype pollution などへの構造的防御を使えます。セッション、ロール、権限、テナント分離はアプリ側の責務です。
デプロイは Node.js、Bun、Deno、Vercel、Netlify、Cloudflare、AWS、Azure Functions、Azure Static Web Apps、Firebase Functions、Docker、single-file、static export などをサポートします。OpenTelemetry と組み込みメトリクスで HTTP、ミドルウェア、RSC/SSR、サーバー関数、キャッシュを計測できます。
--devtools では RSC ペイロード、キャッシュ、ルート、アウトレット、リモートコンポーネント、ライブコンポーネント、ワーカー、サーバーログを確認できます。
TanStack エコシステムを使うのをやめる必要はありません。相性がよいものは次の通りです。
- クライアントコンポーネント内の TanStack Query
- TanStack Table、Virtual、Form、Store などの UI/クライアントライブラリ
- SPA 風の一部分を意図的に作りたい場合の、クライアント側 TanStack Router
TanStack Router を埋め込む場合は "use client" 境界の下に置きます。通常の @lazarv/react-server ファイルルーターアプリで routeTree.gen や RouterProvider を中心に据える必要はありません。
直接持ち越せないものは、@tanstack/react-start のサーバー関数、Start のサーバールート、Start のミドルウェア設定、Start route の head 規約、routeTree.gen をアプリの主要ルーター契約にする設計、ルートローダーを主要なサーバーデータ層にする設計です。
TanStack Start/Router 開発者が特に違いを感じやすい点は次の通りです。
- データ読み込みの中心はルートローダーではなく、非同期サーバーコンポーネントとリソースです。
- ページは既定で同型ではなく、サーバーコンポーネントです。
- RSC はクライアントルーターの周りの実験的な追加機能ではなく、既定の描画プロトコルです。
- 通常のファイルルーターアプリでは
Routeexport、routeTree.gen、RouterProviderは不要です。 - ミドルウェアは builder chain ではなく、ルートツリーに沿ったファイルです。
- キャッシュはルートローダーの SWR キャッシュだけではなく、ディレクティブ/プロバイダー方式です。
- 検索パラメータは型付け/検証できますが、TanStack Router の JSON 検索モデルではありません。
"use client"ページはクライアント専用ルートになり、別のクライアントルーターなしでローカル state を保持できます。- サーバー関数は
createServerFnではなく"use server"を使います。 - ハイドレーションアイランド、ライブコンポーネント、ワーカー、リモートコンポーネント、MCP エンドポイントはランタイム機能です。
アプリが根本的にクライアント主導で、ルートローダーと URL 検索状態が設計の中心であり、TanStack Router の JSON 検索セマンティクスや TanStack Query 中心のデータ層が重要なら、TanStack Start/Router に近いままにするのが自然です。
サーバー優先の RSC ランタイム、明示的なサーバー/クライアント境界、非同期サーバーコンポーネント中心のデータ読み込み、ルートスコープのミドルウェア、堅牢化されたサーバー関数、プロバイダー方式のキャッシュ、ハイドレーションアイランド、ライブコンポーネント、ワーカー、RSC ネイティブなマイクロフロントエンド、幅広いデプロイ先が必要なら、@lazarv/react-server を検討してください。
より広い機能比較は 比較表 を参照してください。設計上の制約とトレードオフは アーキテクチャのトレードオフ を読んでください。