tanstack-router について
tanstack-router は、React と Solid のアプリ向けの型安全なルーターです。ファイルベースのルーティング、検索パラメーター管理、データ読み込み、コード分割を備え、TypeScript と深く連携します。
主な機能
- 01ファイルベースとネストされたルーティング
- 02型安全なパスと検索パラメーター
- 03ルートデータローダー
- 04コード分割とプリロード
- 05ナビゲーションのブロックとリダイレクト
活用シーン
- 01React または Solid アプリのルート構成
- 02URL 検索パラメーターの検証と取得
- 03ルートに応じたデータ読み込み
- 04ルートコンポーネントの遅延読み込み
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ tanstack-skills/ tanstack-skills --skill tanstack-routerインストール後、Claude Code で/tanstack-routerを入力して読み込まれたか確認します。
詳細
概要
TanStack Router は、React(および Solid)アプリケーション向けの完全な型安全性を備えたルーターです。ファイルベースルーティング、検索パラメーターのファーストクラスな管理、組み込みのデータ読み込み、コード分割、TypeScript との深い統合を提供します。フルスタックフレームワークである TanStack Start のルーティング基盤として機能します。
パッケージ: @tanstack/react-router
CLI: @tanstack/router-cli または @tanstack/router-plugin (Vite/Rspack/Webpack)
開発者ツール: @tanstack/react-router-devtools
インストール
npm install @tanstack/react-router
# For file-based routing with Vite:
npm install -D @tanstack/router-plugin
# Or standalone CLI:
npm install -D @tanstack/router-cli
コアコンセプト
ルートツリー
ルートはツリー構造で整理されます。ルートルートが最上位のレイアウトとなり、その下に子ルートがネストされます。
import { createRootRoute, createRoute, createRouter } from '@tanstack/react-router'
const rootRoute = createRootRoute({
component: RootLayout,
})
const indexRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/',
component: HomePage,
})
const aboutRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/about',
component: AboutPage,
})
const routeTree = rootRoute.addChildren([indexRoute, aboutRoute])
const router = createRouter({ routeTree })
ファイルベースルーティング
ファイルベースルーティングでは、ファイル構造からルートツリーが自動的に生成されます。Vite プラグインで設定します。
// vite.config.ts
import { defineConfig } from 'vite'
import { TanStackRouterVite } from '@tanstack/router-plugin/vite'
export default defineConfig({
plugins: [
TanStackRouterVite(),
// ... other plugins
],
})
ファイル命名規則
| ファイルパターン | ルートの種類 | パスの例 |
|---|---|---|
__root.tsx | ルートレイアウト | N/A(すべてをラップ) |
index.tsx | インデックスルート | / |
about.tsx | 静的ルート | /about |
$postId.tsx | 動的パラメーター | /posts/$postId |
posts.tsx | レイアウトルート | /posts/*(レイアウト) |
posts/index.tsx | ネストされたインデックス | /posts |
posts/$postId.tsx | ネストされた動的ルート | /posts/123 |
posts_.$postId.tsx | パスレスレイアウト | /posts/123(異なるレイアウト) |
_layout.tsx | パスレスレイアウト | N/A(ルートをグループ化) |
_layout/dashboard.tsx | グループ化されたルート | /dashboard |
$.tsx | スプラット/キャッチオール | /* |
posts.$postId.edit.tsx | ドット記法 | /posts/123/edit |
特殊なプレフィックス
_プレフィックス:パスレスルート(URL セグメントを持たないレイアウトグループ)$プレフィックス:動的パスパラメーター(folder)の丸括弧:ルートグループ(整理用で、URL に影響しません)
ルート設定
各ルートでは、次の項目を定義できます。
// routes/posts.$postId.tsx
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/posts/$postId')({
// Validation for path params
params: {
parse: (params) => ({ postId: Number(params.postId) }),
stringify: (params) => ({ postId: String(params.postId) }),
},
// Search params validation
validateSearch: (search: Record<string, unknown>) => {
return {
page: Number(search.page ?? 1),
filter: (search.filter as string) || '',
}
},
// Data loading
loader: async ({ params, context, abortController }) => {
return fetchPost(params.postId)
},
// Loader dependencies (re-run loader when these change)
loaderDeps: ({ search }) => ({ page: search.page }),
// Stale time for cached loader data
staleTime: 5_000,
// Preloading
preloadStaleTime: 30_000,
// Error component
errorComponent: PostErrorComponent,
// Pending/loading component
pendingComponent: PostLoadingComponent,
// 404 component
notFoundComponent: PostNotFoundComponent,
// Before load hook (authentication, redirects)
beforeLoad: async ({ context, location }) => {
if (!context.auth.isAuthenticated) {
throw redirect({
to: '/login',
search: { redirect: location.href },
})
}
},
// Head/meta management
head: () => ({
meta: [{ title: 'Post Details' }],
}),
// Component
component: PostComponent,
})
function PostComponent() {
const { postId } = Route.useParams()
const post = Route.useLoaderData()
const { page, filter } = Route.useSearch()
return <div>{post.title}</div>
}
データ読み込み
ルートローダー
export const Route = createFileRoute('/posts')({
loader: async ({ context }) => {
// Access router context (e.g., queryClient)
const posts = await context.queryClient.ensureQueryData({
queryKey: ['posts'],
queryFn: fetchPosts,
})
return { posts }
},
component: PostsComponent,
})
function PostsComponent() {
const { posts } = Route.useLoaderData()
// ...
}
ローダーの依存関係
ローダーを再実行するタイミングを制御します。
export const Route = createFileRoute('/posts')({
loaderDeps: ({ search: { page, filter } }) => ({ page, filter }),
loader: async ({ deps: { page, filter } }) => {
return fetchPosts({ page, filter })
},
})
データの遅延読み込み
重要度の低いデータをストリーミングします。
import { Await, defer } from '@tanstack/react-router'
export const Route = createFileRoute('/dashboard')({
loader: async () => {
const criticalData = await fetchCriticalData()
const deferredData = defer(fetchSlowData())
return { criticalData, deferredData }
},
component: DashboardComponent,
})
function DashboardComponent() {
const { criticalData, deferredData } = Route.useLoaderData()
return (
<div>
<CriticalSection data={criticalData} />
<Suspense fallback={<Loading />}>
<Await promise={deferredData}>
{(data) => <SlowSection data={data} />}
</Await>
</Suspense>
</div>
)
}
コンテキストベースのデータ読み込み
ルーターコンテキストを介して共有の依存関係を提供します。
// Create router with context
const router = createRouter({
routeTree,
context: {
queryClient,
auth: undefined!, // Will be provided by RouterProvider
},
})
// In root/app component
function App() {
const auth = useAuth()
return <RouterProvider router={router} context={{ auth }} />
}
// In routes
export const Route = createFileRoute('/protected')({
beforeLoad: ({ context }) => {
if (!context.auth.user) throw redirect({ to: '/login' })
},
loader: ({ context }) => {
return context.queryClient.ensureQueryData(userQueryOptions())
},
})
検索パラメーター
バリデーション
import { z } from 'zod'
const postSearchSchema = z.object({
page: z.number().default(1),
filter: z.string().default(''),
sort: z.enum(['date', 'title']).default('date'),
})
export const Route = createFileRoute('/posts')({
validateSearch: postSearchSchema,
// Or manual validation:
// validateSearch: (search) => postSearchSchema.parse(search),
})
検索パラメーターの読み取り
function PostsComponent() {
// From route
const { page, filter, sort } = Route.useSearch()
// Or from any component with useSearch hook
const search = useSearch({ from: '/posts' })
}
検索パラメーターの更新
import { useNavigate } from '@tanstack/react-router'
function Pagination() {
const navigate = useNavigate()
const { page } = Route.useSearch()
return (
<button
onClick={() =>
navigate({
search: (prev) => ({ ...prev, page: prev.page + 1 }),
})
}
>
Next Page
</button>
)
}
// Or via Link component
<Link
to="/posts"
search={(prev) => ({ ...prev, page: 2 })}
>
Page 2
</Link>
検索パラメーターのオプション
const router = createRouter({
routeTree,
// Custom serialization
search: {
strict: true, // Reject unknown params
},
// Default search param serializer
stringifySearch: defaultStringifySearch,
parseSearch: defaultParseSearch,
})
ナビゲーション
Link コンポーネント
import { Link } from '@tanstack/react-router'
// Static route
<Link to="/about">About</Link>
// Dynamic route with params
<Link to="/posts/$postId" params={{ postId: '123' }}>
Post 123
</Link>
// With search params
<Link to="/posts" search={{ page: 2, filter: 'react' }}>
Page 2
</Link>
// Active link styling
<Link
to="/posts"
activeProps={{ className: 'active' }}
inactiveProps={{ className: 'inactive' }}
activeOptions={{ exact: true }}
>
Posts
</Link>
// Preloading
<Link to="/posts" preload="intent">Posts</Link>
<Link to="/dashboard" preload="viewport">Dashboard</Link>
// Hash
<Link to="/docs" hash="api-reference">API Reference</Link>
プログラムによるナビゲーション
import { useNavigate, useRouter } from '@tanstack/react-router'
function MyComponent() {
const navigate = useNavigate()
const router = useRouter()
// Navigate to a route
navigate({ to: '/posts', search: { page: 1 } })
// Navigate with replace
navigate({ to: '/posts', replace: true })
// Relative navigation
navigate({ to: '.', search: (prev) => ({ ...prev, page: 2 }) })
// Go back/forward
router.history.back()
router.history.forward()
// Invalidate and reload current route
router.invalidate()
}
リダイレクト
import { redirect } from '@tanstack/react-router'
// In beforeLoad or loader
throw redirect({
to: '/login',
search: { redirect: location.href },
// Optional status code
statusCode: 301, // Permanent redirect (SSR)
})
ナビゲーションのブロック
import { useBlocker } from '@tanstack/react-router'
function FormComponent() {
const [isDirty, setIsDirty] = useState(false)
useBlocker({
shouldBlockFn: () => isDirty,
withResolver: true, // Shows confirm dialog
})
// Or with custom UI
const { proceed, reset, status } = useBlocker({
shouldBlockFn: () => isDirty,
})
if (status === 'blocked') {
return (
<div>
<p>Are you sure you want to leave?</p>
<button onClick={proceed}>Leave</button>
<button onClick={reset}>Stay</button>
</div>
)
}
}
コード分割
自動(ファイルベースルーティング)
ファイルベースルーティングでは、lazy ファイルを作成します。
routes/
posts.tsx # Critical: loader, beforeLoad, meta
posts.lazy.tsx # Lazy: component, pendingComponent, errorComponent
// posts.tsx (loaded eagerly)
export const Route = createFileRoute('/posts')({
loader: () => fetchPosts(),
})
// posts.lazy.tsx (loaded lazily)
import { createLazyFileRoute } from '@tanstack/react-router'
export const Route = createLazyFileRoute('/posts')({
component: PostsComponent,
pendingComponent: PostsLoading,
errorComponent: PostsError,
})
手動でのコード分割
const postsRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/posts',
loader: () => fetchPosts(),
}).lazy(() => import('./posts.lazy').then((d) => d.Route))
プリロード
// Router-level defaults
const router = createRouter({
routeTree,
defaultPreload: 'intent', // 'intent' | 'viewport' | 'render' | false
defaultPreloadStaleTime: 30_000, // 30 seconds
})
// Route-level
export const Route = createFileRoute('/posts/$postId')({
// Stale time for the loader data
staleTime: 5_000,
// How long preloaded data stays fresh
preloadStaleTime: 30_000,
})
// Link-level
<Link to="/posts" preload="intent" preloadDelay={100}>
Posts
</Link>
型安全性
ルーター型の登録
// Declare module for type inference
declare module '@tanstack/react-router' {
interface Register {
router: typeof router
}
}
型安全なフック
すべてのフックは、ルートツリーに基づいて完全に型付けされています。
// useParams - typed to route's params
const { postId } = useParams({ from: '/posts/$postId' })
// useSearch - typed to route's search schema
const { page } = useSearch({ from: '/posts' })
// useLoaderData - typed to loader return
const data = useLoaderData({ from: '/posts/$postId' })
// useRouteContext - typed to route context
const { auth } = useRouteContext({ from: '/protected' })
ルートのジェネリクス
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/posts/$postId')({
// TypeScript infers:
// params: { postId: string }
// search: validated search schema type
// loaderData: return type of loader
// context: router context type
})
認証済みルート
// __root.tsx
export const Route = createRootRouteWithContext<{
auth: AuthContext
}>()({
component: RootComponent,
})
// _authenticated.tsx (pathless layout for auth)
export const Route = createFileRoute('/_authenticated')({
beforeLoad: ({ context, location }) => {
if (!context.auth.isAuthenticated) {
throw redirect({
to: '/login',
search: { redirect: location.href },
})
}
},
})
// _authenticated/dashboard.tsx
export const Route = createFileRoute('/_authenticated/dashboard')({
component: Dashboard, // Only accessible when authenticated
})
スクロール位置の復元
const router = createRouter({
routeTree,
// Enable scroll restoration
defaultScrollRestoration: true,
})
// Or per-route
export const Route = createFileRoute('/posts')({
// Scroll to top on navigation
scrollRestoration: true,
})
// Custom scroll restoration key
<ScrollRestoration
getKey={(location) => location.pathname}
/>
ルートマスキング
実際のルートとは異なる URL を表示します。
<Link
to="/photos/$photoId"
params={{ photoId: photo.id }}
mask={{ to: '/photos', search: { photoId: photo.id } }}
>
View Photo
</Link>
// Or programmatically
navigate({
to: '/photos/$photoId',
params: { photoId: photo.id },
mask: { to: '/photos', search: { photoId: photo.id } },
})
Not Found の処理
// Global 404
const router = createRouter({
routeTree,
defaultNotFoundComponent: () => <div>Page not found</div>,
})
// Route-level 404
export const Route = createFileRoute('/posts/$postId')({
loader: async ({ params }) => {
const post = await fetchPost(params.postId)
if (!post) throw notFound()
return post
},
notFoundComponent: () => <div>Post not found</div>,
})
Head の管理
export const Route = createFileRoute('/posts/$postId')({
head: ({ loaderData }) => ({
meta: [
{ title: loaderData.title },
{ name: 'description', content: loaderData.excerpt },
{ property: 'og:title', content: loaderData.title },
],
links: [
{ rel: 'canonical', href: `https://example.com/posts/${loaderData.id}` },
],
}),
})
TanStack Query との統合
import { queryOptions } from '@tanstack/react-query'
const postsQueryOptions = queryOptions({
queryKey: ['posts'],
queryFn: fetchPosts,
})
export const Route = createFileRoute('/posts')({
loader: ({ context: { queryClient } }) => {
// Ensure data is in cache, won't refetch if fresh
return queryClient.ensureQueryData(postsQueryOptions)
},
component: PostsComponent,
})
function PostsComponent() {
// Use the same query options for reactive updates
const { data: posts } = useSuspenseQuery(postsQueryOptions)
return <PostsList posts={posts} />
}
ルーターフックのリファレンス
| フック | 目的 |
|---|---|
useRouter() | ルーターインスタンスにアクセス |
useRouterState() | ルーターの状態を購読 |
useParams() | ルートのパスパラメーターを取得 |
useSearch() | 検証済みの検索パラメーターを取得 |
useLoaderData() | ルートローダーのデータを取得 |
useRouteContext() | ルートコンテキストを取得 |
useNavigate() | ナビゲーション関数を取得 |
useLocation() | 現在のロケーションを取得 |
useMatches() | マッチしたすべてのルートを取得 |
useMatch() | 特定のルートマッチを取得 |
useBlocker() | ナビゲーションをブロック |
useLinkProps() | カスタムコンポーネント用のリンクプロパティを取得 |
useMatchRoute() | ルートがマッチするかを確認 |
ベストプラクティス
- ほとんどのアプリケーションではファイルベースのルーティングを使用します。よりシンプルで、ルートツリーが自動生成されます
- 型安全性を確保するため、検索パラメーターを Zod またはカスタムバリデーターで検証します
- 検索パラメーターの変更に応じてローダーを再実行するタイミングを制御するには、
loaderDepsを使用します - 依存性の注入(QueryClient、認証状態)にはコンテキストを活用します
- 認証ガードにはコンポーネントではなく、
beforeLoadを使用します - クリティカルなコードと遅延読み込みコードを分けます。ローダーはメインファイルに、コンポーネントは
.lazy.tsxに記述します - 体感パフォーマンスを向上させるには、リンクに
preload="intent"を使用します - ナビゲーション中の不要な再取得を防ぐには、
staleTimeを使用します - アプリ全体で TypeScript の完全な型推論を有効にするため、ルーターの型を登録します
- 404 の状態を条件付きレンダリングで処理する代わりに、
notFound()を使用します - 検索パラメーターのロジックは、それを管理するルートと同じ場所に配置します
- URL セグメントを追加せずに認証やレイアウトのロジックを共有するには、パスレスレイアウト(
_authenticated)を使用します
よくある落とし穴
- ルーターの型の登録(
declare module)を忘れる - ローダーが検索パラメーターに依存しているのに
loaderDepsを使用しない(古いデータが表示される原因になります) beforeLoadではなくコンポーネントに認証チェックを記述する(保護されたコンテンツが一瞬表示されます)pendingComponentでローディング状態を処理しない- ルートローダーではなく、データ取得に
useEffectを使用する - navigate/Linkを使用せず、検索パラメーターを直接変更する
- アプリを
RouterProviderでラップしない - コードベースのルート定義で
getParentRouteを忘れる