TanStack Routerは、ReactとSolid向けのクライアントファーストなルーティングライブラリです。ルートのパス・パスパラメータ・検索パラメータがすべて型として伝播するため、存在しないパスへのLinkやタイプミスがコンパイル時に落ちます。この記事では1.170系を前提に、導入手順、ファイル命名規則、loaderのキャッシュ既定値、SPA配信時のhistory選択までを公式ドキュメントの実装例で確認し、React Router v8との設計差と2026年5月のnpm供給網侵害への対処もあわせて整理します。
まとめ
- 型推論をモジュール宣言のマージで全体へ伝播させる設計で、ルート定義・遷移・パラメータ取得が一続きに型安全になります。ライセンスはMIT、2026年9月時点の最新は
@tanstack/react-router1.170.34です。 - 推奨はファイルベースルーティング。バンドラープラグインが
routeTree.gen.tsを生成し、__root.tsxや$postIdといった命名規則がそのままルートツリーになります。 loaderのキャッシュ既定値はstaleTimeが0ミリ秒、プリロード分の鮮度が30秒、gcTimeとpreloadGcTimeが5分。先読み自体は既定で無効で、defaultPreload: 'intent'を書いて初めて有効になります。- 静的配信ではindex.htmlへ書き換えできるかでbrowser historyとhash historyを選び分け、Electronなどブラウザ以外ではmemory historyを使います。
- 2026年5月11日のnpm侵害で1.169.5と1.169.8に悪性コードが混入しました。1.169.9以上への更新と、当日インストールした環境の資格情報ローテーションが必要です。
TanStack Routerの現在地と対応環境
React Locationから1.0系へ至る経緯と現行版
前身は@tanstack/react-locationです。公式の移行ガイドは両者の差を、型推論の方式がジェネリクスからモジュール宣言のマージ(module declaration merging)へ変わった点と、ルート定義が単一配列からルートツリーへ変わった点として説明しており、単なる改称ではありません。v1.0.0はGitHubのリリース記録で2023年12月23日に公開されました。
その後のリリース間隔は短く、npmレジストリのdist-tag latestは1.170.34でした(2026年9月10日確認)。マイナー番号が週単位で動くため、記事のバージョン表記ではなくpackage.jsonとnpm view @tanstack/react-router versionの実際の値を基準に読み替えてください。
対応フレームワークと動作要件
公式ドキュメントは対応範囲をTanStack Router is currently only compatible with React (with ReactDOM) and Solid
と明記しています。React NativeやAngular、Vueへの対応はコントリビューションを募っている段階です。npmには@tanstack/vue-routerも公開されていますが(2026年9月10日時点で1.170.31)、ドキュメントの対応表記には含まれないため、業務利用の判断はReactとSolidに限るのが安全です。
| 項目 | 要件・値 | 出典 |
|---|---|---|
| Reactパッケージ | @tanstack/react-router 1.170.34 | npm registry(2026-09-10確認) |
| Solidパッケージ | @tanstack/solid-router 1.170.32 | npm registry(2026-09-10確認) |
| React / ReactDOM | 18以上(createRoot対応)または19 | 公式Quick Start |
| TypeScript | 5.3以上を推奨(必須ではない) | 公式Quick Start |
| Node.js | 20.19以上(engines指定) | npm registry |
| ライセンス | MIT | npm registry / GitHub |
TypeScriptは必須要件ではありません。ただしJavaScriptだけの構成では、このライブラリの中心にある型推論の恩恵は受けられず、比較軸は検索パラメータの管理と内蔵キャッシュだけに絞られます。
React Router v8・Next.jsとの違い
React Router v8との設計差
React Routerも現行のv8(npmの最新は8.3.1)でloaderやactionを備え、フレームワークモードではファイルベースのルート定義も使えます。差が出るのは、型の伝わり方と検索パラメータの扱いです。
| 観点 | TanStack Router | React Router v8 |
|---|---|---|
| 型推論 | Register宣言で全体へ伝播 | 生成された型定義を各ルートで参照 |
| 検索パラメータ | validateSearchでスキーマ検証 | URLSearchParams中心 |
| データ取得 | loader+SWRキャッシュ内蔵 | loader(キャッシュは外部) |
| SSR | 手動手順あり・Start推奨 | フレームワークモードが担当 |
| 先読み | defaultPreloadで一括制御 | Linkのprefetch属性 |
React Router v7からの移行手順は公式が用意しており、所要時間の目安をアプリの複雑さに応じて2〜4時間、難易度をIntermediateとしています。既存アプリのルート数が多いほど、クエリ文字列を素の文字列として扱っていた箇所をスキーマ付きに書き直す工数が支配的になります。React Router側の実装はReact Routerのmeta・title設定と基本ルーティング|v8対応入門とReact Routerのaction・loaderの使い方|clientAction/clientLoaderの違いとv8対応で扱っています。
Next.jsやTanStack Startとの選び分け
TanStack Router単体でもSSRは組めます。公式のhow-toはExpressとcompressionを入れて自前のサーバーに統合する手順を載せており、既存サーバーへ載せる場合はこちらです。ただし同じページでTanStack Start is the recommended way to set up SSR
と明記されており、SSR・ストリーミング・デプロイまでまとめて扱うならTanStack Startとは?フルスタックReactフレームワークの特徴・使い方・デプロイ【2026年版】、あるいはNext.jsが対象になります。
逆に、APIサーバーが別にあってフロントは静的配信するだけの構成なら、Next.jsを持ち込む理由はほとんどありません。管理画面のように検索条件をURLへ載せて共有する画面が多いほど、TanStack Routerのパラメータ管理が効きます。
導入手順とVite設定
既存プロジェクトへの追加
既存のReactプロジェクトに入れる場合は、本体と開発用のプラグイン・Devtoolsを追加します。以降のコードはいずれも要点の抜粋で、実際のファイルではスタイルや周辺のimportが加わります。
npm install @tanstack/react-router
npm install -D @tanstack/router-plugin @tanstack/react-router-devtools
ファイルベースルーティングを使うなら、バンドラー側にプラグインを登録します。公式の移行ガイドはReactプラグインより前に置くよう指示しています。
// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { tanstackRouter } from '@tanstack/router-plugin/vite'
export default defineConfig({
plugins: [
tanstackRouter(), // react プラグインより前に置く
react(),
],
})
ルートの置き場所と生成先はプロジェクト直下のtsr.config.jsonで指定します。
{
"routesDirectory": "./src/routes",
"generatedRouteTree": "./src/routeTree.gen.ts",
"quoteStyle": "single"
}
@tanstack/router-pluginが対応するバンドラーはVite、Rspack/Rsbuild、Webpack、Esbuildです。peerDependenciesの指定はVite 5から8、webpack 5.92以上なので、Vite以外の構成でもファイルベースルーティングを諦める必要はありません。
新規プロジェクトの雛形とDevtools
新規に作る場合、現行のQuick Startは@tanstack/cliを使う手順を案内しています。ファイルベースかコードベースか、TypeScriptを使うかといった選択をプロンプトで答える形式です。
npx @tanstack/cli create --router-only
従来案内されていたcreate-tsrouter-appもnpm上では更新が続いていますが(2026年9月10日時点で0.54.43)、ドキュメントの導線は@tanstack/cliへ移っています。新規に選ぶなら後者に合わせてください。ルートの一致状況やloaderの状態を確認するDevtoolsは@tanstack/react-router-devtoolsとして別パッケージで提供され、ルートコンポーネントに置くだけで動きます。一致したルート、各ルートのloaderの状態、現在の検索パラメータの値をその場で確認できます。
ルート定義の2方式とファイル命名規則
ファイルベースルーティングの命名規則
ファイルベースでは、ファイル名がそのままルートツリーになります。規則は公式ドキュメントの表にまとまっており、実務で使う頻度が高いものは次のとおりです。
| 記法 | 意味 | 例 |
|---|---|---|
| __root.tsx | ルートルート(必須・routesDirectory直下) | src/routes/__root.tsx |
| . 区切り | 入れ子のルート | blog.post.tsx |
| $ 接頭 | パスパラメータ | posts.$postId.tsx |
| _ 接頭 | パスを持たないレイアウトルート | _app.tsx |
| _ 接尾 | 親の入れ子から除外 | posts_.tsx |
| – 接頭 | ルートツリーから除外(同居用) | -components/ |
| (folder) | URLに出ないルートグループ | (auth)/login.tsx |
| index | 親パスと完全一致で表示 | posts.index.tsx |
-接頭のファイルがルートツリーから除外される点は、ルート専用のコンポーネントやフックを同じフォルダへ置きたいときに効きます。
個々のルートファイルはcreateFileRouteでエクスポートします。第1引数のパス文字列はプラグインが生成・更新するため、手で書き換える必要はありません。
// src/routes/posts.$postId.tsx
import { createFileRoute } from '@tanstack/react-router'
import { fetchPost } from '../api/posts' // 自前のデータ取得関数
export const Route = createFileRoute('/posts/$postId')({
loader: ({ params }) => fetchPost(params.postId),
component: PostComponent,
})
function PostComponent() {
const { postId } = Route.useParams()
return <div>投稿ID: {postId}</div>
}
createRouterによる生成と型の登録
生成されたルートツリーをcreateRouterに渡し、declare moduleで型を登録します。この宣言がプロジェクト全体へ型を伝播させる仕組みで、これを書き忘れるとLinkのtoが補完されません。
// src/router.tsx
import { createRouter } from '@tanstack/react-router'
import { routeTree } from './routeTree.gen'
export const router = createRouter({ routeTree })
declare module '@tanstack/react-router' {
interface Register {
// ルーターの型をプロジェクト全体へ登録する
router: typeof router
}
}
あとはエントリーポイントでRouterProviderに渡すだけです。
// src/main.tsx
import ReactDOM from 'react-dom/client'
import { RouterProvider } from '@tanstack/react-router'
import { router } from './router'
ReactDOM.createRoot(document.getElementById('root')!).render(
<RouterProvider router={router} />,
)
createRootRouteとcreateRouteを手で組むコードベース定義も残っていますが、公式は大半のユースケースで推奨しないと明言しています。両方式の混在は可能なので、既存資産があるなら段階的に移せます。
Outletによるレイアウトのネスト
入れ子になった子ルートを描画する場所を指定するのがOutletです。propsを取らず、ルートのコンポーネントツリーのどこにでも置けます。一致する子ルートが無ければnullを描画します。
// src/routes/__root.tsx
import { createRootRoute, Outlet } from '@tanstack/react-router'
export const Route = createRootRoute({
component: RootComponent,
})
function RootComponent() {
return (
<div>
<h1>My App</h1>
<Outlet />
</div>
)
}
手数が減る挙動として、ルートのcomponentを省略すると自動でOutletだけが描画されます。共通の枠を持たない中間ルートに、Outletを返すだけのコンポーネントを書く必要はありません。ヘッダーだけ違う画面群は、_接頭のパスなしレイアウトルートを分けてそれぞれにOutletを置きます。
Linkとナビゲーション
Linkの型安全な遷移とアクティブ表示
Linkのtoには登録済みのルートパスだけが渡せます。パラメータ付きのパスではparamsが必須になり、渡し忘れは型エラーになります。
import { Link } from '@tanstack/react-router'
<Link
to="/posts/$postId"
params={{ postId: 'my-first-post' }}
activeProps={{ className: 'font-bold' }}
>
記事を読む
</Link>
アクティブ状態の見た目はactivePropsとinactivePropsで切り替え、判定条件はactiveOptionsで完全一致やハッシュの扱いを指定します。相対指定も使え、to="."で現在ルートの再読み込み、to=".."で1つ上への遷移になります。
フォーム送信後のように副作用の結果として遷移する場合はuseNavigateを使います。公式は可能な限りLinkコンポーネントを使うよう推奨しており、命令的な遷移は副作用の後始末に限るのが原則です。
import { useNavigate } from '@tanstack/react-router'
function CreatePostForm() {
const navigate = useNavigate({ from: '/posts' })
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
const response = await fetch('/posts', { method: 'POST' })
const { id: postId } = await response.json()
if (response.ok) {
navigate({ to: '/posts/$postId', params: { postId } })
}
}
return <form onSubmit={handleSubmit}>{/* ... */}</form>
}
ルート経由でRoute.useNavigate()と呼ぶとfromがそのルートに固定されるため、相対パスを多用する画面では指定漏れによる遷移先ずれを防げます。
パスパラメータと検索パラメータの型安全な取得
パスパラメータの取得
パスパラメータはルートのRoute.useParams()から取り出します。$postIdというファイル名からpostIdというキーが型として生えるため、文字列キーの綴りミスは起きません。
export const Route = createFileRoute('/posts/$postId')({
component: PostComponent,
})
function PostComponent() {
const { postId } = Route.useParams()
return <div>{postId}</div>
}
ルートに紐づかない共通コンポーネントから読むときはuseParams({ strict: false })を使います。この場合は各キーが省略可能な型になるため、値を使う前に未定義かどうかを確認してください。末尾を丸ごと受けるスプラットルートでは_splatというキーに入ります。
検索パラメータのスキーマ検証
検索パラメータはルートのvalidateSearchで検証します。受け取るのはRecord<string, unknown>で、戻り値の型がそのままuseSearchの型になります。次の例はZodを使うためnpm install zodが別途必要です。
import { z } from 'zod'
const productSearchSchema = z.object({
page: z.number().catch(1),
filter: z.string().catch(''),
})
export const Route = createFileRoute('/shop/products')({
validateSearch: productSearchSchema,
})
検証に失敗して例外が投げられるとerror.routerCodeにVALIDATE_SEARCHが設定され、errorComponentが描画されます。壊れたURLを共有されてもアプリ全体が落ちないよう、フォールバック値を持たせるのが公式の推奨です。Valibotのようにスキーマ標準(Standard Schema)を実装したライブラリはアダプタなしで使えます。ルーターを入れ替えずにURLクエリの状態管理だけ型安全にしたい場合は、型安全なURLクエリパラメータ管理ライブラリnuqs 2.5.0アップデートの概要と注目すべき新機能・改善点を徹底解説で扱うnuqsも選択肢です。
loaderによるデータ取得とキャッシュの既定値
loaderによる取得とloaderDepsによるキャッシュキー
ルート単位のデータ取得はloaderが担当します。取得結果はRoute.useLoaderData()で読み出します。ページングのように検索パラメータで結果が変わる場合は、loaderDepsでキャッシュキーに含める値を明示します。
export const Route = createFileRoute('/posts')({
validateSearch: z.object({ offset: z.number().catch(0), limit: z.number().catch(20) }),
loaderDeps: ({ search: { offset, limit } }) => ({ offset, limit }),
loader: ({ deps: { offset, limit } }) => fetchPosts({ offset, limit }),
component: PostsComponent,
})
loaderDepsを書かないとdepsは空オブジェクトになり、offset違いの結果が同じキャッシュ項目として扱われます。一覧画面でページを送っても内容が変わらない不具合の典型的な原因です。
キャッシュと先読みの既定値
内蔵キャッシュと先読みの既定値は、公式のRouterOptionsとPreloadingガイドに数値で書かれています。挙動を勘で調整する前に、次の5つを押さえてください。
| オプション | 既定値 | 意味 |
|---|---|---|
| staleTime | 0ミリ秒 | 再訪時に背景で再検証する |
| preloadStaleTime | 30秒 | 先読みしたデータを新鮮とみなす時間 |
| gcTime / preloadGcTime | 5分 | 未使用データを保持する時間 |
| defaultPreload | false | 先読みは既定で無効 |
| defaultPreloadDelay | 50ミリ秒 | ホバー・表示から先読み開始まで |
先読みが既定で無効である点は誤解されがちです。ホバーで次画面を取りに行かせるには、ルーター生成時に明示します。
const router = createRouter({
routeTree,
defaultPreload: 'intent',
defaultPreloadStaleTime: 10_000,
})
'intent'はホバーとtouchstart、'viewport'はIntersection Observerによる表示検知、'render'はリンクの描画直後です。一覧が長い画面で'viewport'を選ぶと画面に入っただけで全リンク分のリクエストが飛ぶため、まず'intent'から始めてください。
TanStack Query併用時のキャッシュ責務
内蔵キャッシュはSWR型で、ルート単位のデータには十分です。同じデータを複数画面で共有する、楽観的更新を行う、ミューテーション後に個別キーだけ無効化する、といった要件が出た時点で足りなくなります。公式は外部キャッシュとの関係を、Routerが取得の調整役(coordinator)、外部ライブラリが保管役という役割分担で説明しています。
線引きは責務で決めます。データの寿命がルートの表示中に閉じるならloaderだけで完結させ、画面をまたいで生き続けるならQueryへ持たせ、loaderはその読み込みを待つ役に徹します。鮮度の判断まで外部キャッシュへ委ねる場合は、公式のとおりdefaultPreloadStaleTime: 0を指定してRouter側の鮮度判定を無効化します。併用の作法はTanStack Queryとは?React Queryとの違い・v5の使い方と脆弱性対策で整理しています。
404とエラーの扱い
一致するルートが無い場合とloader内でデータが見つからない場合は、どちらもnotFound()とnotFoundComponentで処理します。かつてのNotFoundRouteは非推奨で、将来のリリースで削除予定です。
export const Route = createFileRoute('/posts/$postId')({
loader: async ({ params: { postId } }) => {
const post = await getPost(postId)
if (!post) throw notFound()
return { post }
},
notFoundComponent: () => <p>この記事は見つかりませんでした</p>,
})
既定のnotFoundModeは'fuzzy'で、最も近い一致ルートのうちnotFoundComponentを持つものが処理します。1つの404画面に集約するなら'root'に変更します。注意点は、子を持たないリーフルートがOutletを描画せず404を処理できないことです。全ルートを網羅するにはcreateRouterへdefaultNotFoundComponentを渡します。取得失敗などの例外はerrorComponentが受け持ち、404とは別系統で扱われます。
SPAとして配信するときのhistoryとbasepath
history種別の選び分け
ルーターはhistoryの抽象を内部に持ち、指定しなければブラウザ向けの実装が自動で作られます。@tanstack/historyが提供する3種類は配信先の制約で選びます。
| 生成関数 | 使いどころ | URLの形 |
|---|---|---|
| createBrowserHistory | 既定。index.htmlへ書き換えできるサーバー | /posts/1 |
| createHashHistory | 書き換えできない配信環境 | /#/posts/1 |
| createMemoryHistory | ブラウザ以外、URLを触らせたくない場合 | URLに出ない |
公式はhash historyの用途をHash routing can be helpful if your server doesn’t support rewrites to index.html for HTTP requests
と説明しています。リライト設定を持てない静的ホスティングでは、browser historyのままだと直リンクが404になります。
サブパス配信とデスクトップアプリ
アプリをドメイン直下ではなくサブパスに置く場合はbasepathを指定します。既定値は/で、ルーターインスタンス全体の起点になります。
import { createMemoryHistory, createRouter } from '@tanstack/react-router'
const memoryHistory = createMemoryHistory({
initialEntries: ['/'],
})
export const router = createRouter({
routeTree,
history: memoryHistory,
basepath: '/admin',
})
Electronのようにレンダラープロセスでファイルを直接読み込む構成では、URLバーが無く履歴APIに載せる必要もないため、memory historyが選択肢になります。テストでも同じ理由で使えます。initialEntriesに初期URLを渡すだけで、任意の画面から描画を始められます。
2026年5月のnpm侵害と依存バージョンの点検
TanStack Routerを新規に採用するなら、機能より先に確認すべき事故があります。2026年5月11日の19時20分から19時26分(UTC)にかけて、42個の@tanstack/*パッケージに合計84個の悪性バージョンがnpmへ公開されました。GitHubのセキュリティアドバイザリはGHSA-g7cv-rxg3-hmpx、CVE番号はCVE-2026-45321、深刻度はCVSS 9.6のCriticalです。
公開はTanStack/routerのGitHub Actions OIDCによる正規のtrusted publisher経由で行われ、公開ワークフロー自体は改変されていません。pull_request_targetの設定不備、fork境界をまたぐActionsキャッシュの汚染、ランナープロセスからのOIDCトークン抽出という3つの既知の弱点を連鎖させた攻撃で、リポジトリのコミット履歴を追っても異常は見つかりません。
インストール時に実行されるペイロードは、AWSのインスタンスメタデータとSecrets Manager、GCPメタデータ、Kubernetesのサービスアカウントトークン、HashiCorp Vaultのトークン、~/.npmrc、GitHubトークン、~/.ssh/配下の秘密鍵を収集します。送信先は秘匿メッセンジャーのファイルアップロード網で、攻撃者が管理するC2サーバーを持たないため、IPやドメインのブロック以外にネットワーク側の打ち手がありません。
| パッケージ | 悪性バージョン | 修正版 |
|---|---|---|
| @tanstack/react-router | 1.169.5 / 1.169.8 | 1.169.9 |
| @tanstack/router-core | 1.169.5 / 1.169.8 | 1.169.9 |
| @tanstack/router-plugin | 1.167.38 / 1.167.41 | 1.167.42 |
| @tanstack/react-router-devtools | 1.166.16 / 1.166.19 | 1.166.20 |
| @tanstack/react-start | 1.167.68 / 1.167.71 | 1.167.72 |
| @tanstack/solid-router | 1.169.5 / 1.169.8 | 1.169.9 |
対応は3つです。第一に、@tanstack/react-routerを1.169.9以上、プラグインとDevtoolsも上表の修正版以上へ上げ、ロックファイルとnode_modulesを削除して入れ直します。第二に、該当バージョンでnpm installやpnpm installを実行した開発機・CIは侵害済みとみなし、その環境から到達できた資格情報をすべてローテーションします。アドバイザリはAll credentials accessible to the install process should be rotated immediately
と明記しています。第三に、クラウドの監査ログをインストール時刻以降までさかのぼって確認します。
混入の有無はpackage.jsonのoptionalDependenciesで見分けられます。悪性版には@tanstack/setupというGitHub参照の依存が入っており、npm packでtarballだけ取得すればライフサイクルスクリプトを実行せずに中身を確認できます。
この事故を理由にTanStack Routerを外す判断は取りません。アドバイザリで影響版と修正版がパッケージ単位に列挙され、検知方法まで公開されているため、利用側で確認と切り戻しができるからです。むしろ危ういのは、バージョンを固定したまま何年も上げない運用です。更新を止めるのではなく、更新の前に公開日と差分を見る運用へ寄せてください。
よくある質問
TanStack Routerは無料で商用利用できますか?
はい。@tanstack/react-routerのライセンスはMITで、npmのパッケージメタデータとGitHubリポジトリの双方で確認できます。有料プランや商用ライセンスの購入は不要です。
React RouterとTanStack Routerはどちらを選ぶべきですか?
検索パラメータをアプリの状態として使う画面が多い場合と、パスやパラメータの型崩れをコンパイル時に止めたい場合はTanStack Routerが向きます。既存のReact Routerアプリで型に困っていないなら、公式が目安とする2〜4時間(アプリの複雑さによる)の移行作業とリグレッション対応の工数を払う理由はありません。
SolidやVueでも使えますか?
Solidは@tanstack/solid-routerで正式に対応しています。npmには@tanstack/vue-routerも公開されていますが、公式ドキュメントが対応フレームワークとして挙げているのはReactとSolidだけです。Vue版を業務で採用する判断は、この差を踏まえて行ってください。
TanStack Queryは必須ですか?
必須ではありません。loaderにSWR型のキャッシュが内蔵されているため、ルート表示中に閉じるデータなら単体で足ります。画面をまたいでデータを共有する、楽観的更新を行うといった要件が出た段階で併用を検討してください。
SSRやサーバー機能まで必要な場合はどうしますか?
TanStack Router単体にも、既存サーバーへ統合する手動SSRの公式手順があります。ただし公式が推奨するのはTanStack Startで、SSR・ストリーミング・デプロイを設定なしで扱えます。SPAとして静的配信するだけならTanStack Router単体で完結します。