TanStack Start 02 ルーティングとデータ読み込み

更新日:

ルーティングは 100% TanStack Router。ここを理解することが Start の理解の9割。

ファイル名とURL

URLファイル種類
/index.tsxインデックス
/aboutabout.tsx静的
(レイアウト)posts.tsx/posts/* を包む
/posts/posts/index.tsxインデックス
/posts/:postIdposts/$postId.tsx(= posts.$postId.tsx)動的($ で始まる)
/rest/*rest/$.tsxワイルドカード
(URLに出ない枠)_authenticated.tsxパスなしレイアウト(_ 始まり)。ログイン必須エリアなど
(整理用)(group)/...グループ(URLに影響なし)
  • . と / は同じ意味(posts.$postId.tsx でも posts/$postId.tsx でも可)
  • routeTree.gen.ts は開発サーバーが自動生成。createFileRoute('/posts/$postId') のパス文字列も自動で書き換わる

ルートの定義

// src/routes/posts/$postId.tsx
import { createFileRoute, notFound } from '@tanstack/react-router'
import { getPost } from '~/utils/posts.functions'
 
export const Route = createFileRoute('/posts/$postId')({
  loader: async ({ params }) => {              // ページ表示前にデータ取得
    const post = await getPost({ data: { id: params.postId } })
    if (!post) throw notFound()
    return post
  },
  head: ({ loaderData }) => ({ meta: [{ title: loaderData?.title }] }),
  component: PostPage,
  pendingComponent: () => <p>Loading...</p>,
  errorComponent: ({ error }) => <p>エラー: {error.message}</p>,
  notFoundComponent: () => <p>記事が見つかりません</p>,
})
 
function PostPage() {
  const post = Route.useLoaderData()           // ★ 型付きで取得
  const { postId } = Route.useParams()
  return <article><h1>{post.title}</h1></article>
}

⚠️ loader は「サーバーとブラウザ両方」で動く

リンクと遷移(型安全)

import { Link, useNavigate } from '@tanstack/react-router'
 
<Link to="/posts/$postId" params={{ postId: '123' }}>記事</Link>
<Link to="/about" activeProps={{ className: 'font-bold' }}>About</Link>
 
const navigate = useNavigate()
navigate({ to: '/posts', search: { page: 2 } })
  • 存在しないパスや足りない params はコンパイルエラーになる(これが最大の売り)
  • 既定でホバー時などにプリロード(設定で調整)

Search Params(URL のクエリを「型付きの状態」として使う)

一覧のページ番号・絞り込み・並び順などを URL に持たせる → 共有・リロード・戻るボタンで状態が保たれる。

// src/routes/shop/products.tsx
import { z } from 'zod'
const productSearchSchema = z.object({
  page: z.number().catch(1),
  filter: z.string().catch(''),
  sort: z.enum(['newest', 'oldest', 'price']).catch('newest'),
})
export const Route = createFileRoute('/shop/products')({
  validateSearch: productSearchSchema,                     // 検証+型付け
  loaderDeps: ({ search }) => ({ page: search.page }),     // search を loader で使う時
  loader: ({ deps }) => fetchProducts({ data: deps }),
  component: ProductList,
})
function ProductList() {
  const { page, sort } = Route.useSearch()
  const navigate = useNavigate({ from: Route.fullPath })
  return <button onClick={() => navigate({ search: (prev) => ({ ...prev, page: prev.page + 1 }) })}>次へ</button>
}
  • .catch() で不正な値は既定値に(ユーザーにエラーを見せない)

beforeLoad(ルートのガード・コンテキスト)

子ルートより先に実行される「ルート用ミドルウェア」。ログイン必須エリアに使う。

// src/routes/_authenticated.tsx(この下のルートは全部ログイン必須)
import { createFileRoute, redirect } from '@tanstack/react-router'
export const Route = createFileRoute('/_authenticated')({
  beforeLoad: async ({ location }) => {
    const user = await getCurrentUser()
    if (!user) {
      throw redirect({ to: '/login', search: { redirect: location.href } })
    }
    return { user }   // 子ルートの context で使える
  },
})
  • ⚠️ 公式注意: ルートのガードは「データの認可」ではない。非公開データを返す Server Function / Server Route 側でも必ず認証・認可する
  • 実行順: params 解析 → validateSearch → beforeLoad(親→子) → loader(並列)

loader のキャッシュ

エラー・404

  • throw notFound() → notFoundComponent
  • throw redirect({ to }) → 遷移
  • 例外 → errorComponent(ルートごと / ルーター全体の既定)

関連