はじめに
普段はNextJSを使っている者ですが、先週こちらの記事の内容を見て、TanStack Startについて気になったので、その開発体験をしてみました。
今回の記事では、それぞれのルーティングを使う際の利点と注意点を見ていきます。
ルーティング思想と書き方の違い
- Next.js: ファイルシステムベース。URLはフォルダ構成
- TanStack: URLは一つの巨大な型定義
例として、以下のページの動的なルーティングの仕方を考えます。
- users
- users/{id}
NextJSの場合
app/
├── users/
│ └── page.tsx // /users
└── user/
└── [id]/
└── page.tsx // /user/123
実装のコード(詳細ページ)
Next.js 15.5からは Route Props Helpers を使うことで、以下のように書けます。
// app/user/[id]/page.tsx
export default async function UserPage({ params }: PageProps<'/user/[id]'>) {
const { id } = await params;
// ディレクトリ構成を読み取って型定義が自動生成されるため、
// PagePropsのジェネリクスでは存在するパスのみ指定可能で安全。
return <div>User ID: {id}</div>;
}
TanStack Start の場合
TanStackでは、まず「このパスには $id という変数が存在する」という契約(Route定義)をコードで記述します。
routes/
├── users.tsx // /users
└── user.$id.tsx // /user/123
// routes/user.$id.tsx
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/user/$id')({
component: UserComponent,
})
function UserComponent() {
// useParams を呼ぶだけで、すでに string 型の id が補完される。
// もしパス定義を /user/$userId に変えたら、ここはエディタがエラーを通知する。
const { id } = Route.useParams()
return <div>User ID: {id}</div>
}
Next.jsとTanStackStartは、どちらもパスとコードの一貫性を保つ書き方をすることで、
パスを変えたのに、コードのPathParamsを変え忘れる、ということが起こらないようになります。
<Link> コンポーネント
<Link> コンポーネントも、Next.jsの typedRoutes を利用する場合の挙動と、TanStackStartのデフォルトの挙動で似たような動きをします。
// next.config.ts(Next.jsの設定)
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
typedRoutes: true, // これを追加すると<Link>コンポーネントのパスをチェックする
};
export default nextConfig;
// NextJSの<Link>
<Link href="/user/123">プロフィールへ</Link> // /user/123は存在するパスなのでOK
<Link href="/users/123">プロフィールへ</Link> // /users/123は存在しないのでNG
// TanStackの<Link>
<Link to="/user/$id" params={{ id: '123' }}>プロフィールへ</Link>
// 1. `to` に存在しないパスを入れようとするとエディタに怒られる
// 2. paramsが[id]なのか、[userId]なのかで、Route定義と一致する必要がある
両方とも存在しないパスにはエラーを表示しますが、
URLの扱いはStringがメインのNext.jsと、to と Params に分解されているTanstackStartで違いがあります。
toとParamsに分かれている利点は、コードを書くときの補完(どのパスにどのパラメータが必要なのか)や、リファクタリングの場面で地味に生きてきそうです。
QueryParams
例えば以下のようなクエリパラメータがある場合、
- users/{id}?tab={postsまたはlikes}
Next.jsではPageProps + zod でコンポーネント内ではQueryParamsが型定義できますが
import { z } from 'zod';
// クエリパラメータのバリデーションスキーマ定義
const userSearchSchema = z.object({
tab: z.enum(['posts', 'likes']).default('posts'),
});
export default async function UserPage({ params, searchParams }: PageProps<'/user/[id]'>) {
const { id } = await params;
// tabにはpostsかlikesしか来ない
const rawSearchParams = await searchParams;
const { tab } = userSearchSchema.parse(rawSearchParams);
return (
<div>
<h1>ユーザーID: {id}</h1> {/* id は string */}
<p>現在のタブ: {tab}</p> {/* tab は 'posts' | 'likes' */}
</div>
);
}
Next.jsの <Link> ではクエリパラメータのバリデーションが効かないため、
パスの間違いは検知されているけど、クエリパラメータが間違っている部分は指摘されません。
TanStackの場合、Routeを定義するときにvalidateSearchを設定すると、
その型を <Link> コンポーネントにも強制できます。
// routes/user.$id.tsx
export const Route = createFileRoute('/user/$id')({
validateSearch: (search) => z.object({
tab: z.enum(['posts', 'likes']).default('posts'),
}).parse(search),
component: UserComponent,
})
function UserComponent() {
const { id } = Route.useParams()
// クエリパラメータの tab は「検証済みの型」として取得
const { tab } = Route.useSearch()
return (
<div>
<h1>ユーザーID: {id}</h1>
<p>現在のタブ: {tab}</p>
</div>
)
}
これに対してLinkを色々書いてみると、クエリパラメータの部分も、
エディタにエラーが表示されます。
TanStackの注意点
エディタにエラーは出ているのですが、
このままでもTanStackはデフォルトでビルドを通しちゃうみたいです。
型チェックをするため、package.jsonを修正する必要がありました。
"build": "tsc && vite build",
これによって、<Link>コンポーネントの安全性 + ビルド時の型チェックも対応できました。
まとめ
- Next.js は、
PagePropsやtypedRoutesを使うことで、パスパラメータの定義や<Link>コンポーネントの安全性が上がりました - 一方で、クエリパラメータにおいては、TanstackStart のガードレールが優秀でした
- 双方適切な設定を行い、ルーターを安全に利用しましょう



