mcpField
T
tanstack-router

tanstack-router

github.com/tanstack-skills開発365.0k 回インストール39 週間前に更新
リポジトリ

tanstack-router について

tanstack-router は、React と Solid のアプリ向けの型安全なルーターです。ファイルベースのルーティング、検索パラメーター管理、データ読み込み、コード分割を備え、TypeScript と深く連携します。

主な機能

  1. 01ファイルベースとネストされたルーティング
  2. 02型安全なパスと検索パラメーター
  3. 03ルートデータローダー
  4. 04コード分割とプリロード
  5. 05ナビゲーションのブロックとリダイレクト

活用シーン

  1. 01React または Solid アプリのルート構成
  2. 02URL 検索パラメーターの検証と取得
  3. 03ルートに応じたデータ読み込み
  4. 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()ルートがマッチするかを確認

ベストプラクティス

  1. ほとんどのアプリケーションではファイルベースのルーティングを使用します。よりシンプルで、ルートツリーが自動生成されます
  2. 型安全性を確保するため、検索パラメーターを Zod またはカスタムバリデーターで検証します
  3. 検索パラメーターの変更に応じてローダーを再実行するタイミングを制御するには、loaderDepsを使用します
  4. 依存性の注入(QueryClient、認証状態)にはコンテキストを活用します
  5. 認証ガードにはコンポーネントではなく、beforeLoadを使用します
  6. クリティカルなコードと遅延読み込みコードを分けます。ローダーはメインファイルに、コンポーネントは.lazy.tsxに記述します
  7. 体感パフォーマンスを向上させるには、リンクにpreload="intent"を使用します
  8. ナビゲーション中の不要な再取得を防ぐには、staleTimeを使用します
  9. アプリ全体で TypeScript の完全な型推論を有効にするため、ルーターの型を登録します
  10. 404 の状態を条件付きレンダリングで処理する代わりに、notFound()を使用します
  11. 検索パラメーターのロジックは、それを管理するルートと同じ場所に配置します
  12. URL セグメントを追加せずに認証やレイアウトのロジックを共有するには、パスレスレイアウト(_authenticated)を使用します

よくある落とし穴

  • ルーターの型の登録(declare module)を忘れる
  • ローダーが検索パラメーターに依存しているのにloaderDepsを使用しない(古いデータが表示される原因になります)
  • beforeLoadではなくコンポーネントに認証チェックを記述する(保護されたコンテンツが一瞬表示されます)
  • pendingComponentでローディング状態を処理しない
  • ルートローダーではなく、データ取得にuseEffectを使用する
  • navigate/Linkを使用せず、検索パラメーターを直接変更する
  • アプリをRouterProviderでラップしない
  • コードベースのルート定義でgetParentRouteを忘れる

コメント