Open8

TanStack Router 公式ドキュメントを読み解く

kaitokaito

Router の作成方法

Creating a Router | TanStack Router React Docs

Router Class

  • まずは、Router インスタンスを作成する必要がある
  • ルートの定義や遷移、URLのパラメータ処理など、すべての「ルーティングロジック」を一括で管理する
  • React Routerでは BrowserRouter がベースになるように、TanStack Routerでは createRouter() で作ったインスタンスが中心となる
import { createRouter } from '@tanstack/react-router'

const router = createRouter({
  // ...
})

Route Tree

  • Router インスタンスを作成するときに routeTree を渡す必要がある
  • routeTree は「ルート階層をツリー状に表現したオブジェクト」であり、下記のような情報が定義されている
    • どんなパスがあるか
    • そのパスにどんなコンポーネントを表示するか
    • 親子関係やネスト構造

Filesystem Route Tree

  • ルーティングをフォルダとファイル構造で管理する方法

  • ViteやNext.jsのように pages/ などのフォルダから自動でルート定義を生成して、 routeTree.gen.ts という自動生成ファイルが作成される

  • 下記のように routeTree をインポートして、createRouter() に渡す

    import { routeTree } from './routeTree.gen'
    

Code-Based Route Tree

  • 自分で route インスタンスを作成して、明示的にルートツリーを組み立てる方法

    import { rootRouteWithLoader, Route } from '@tanstack/react-router'
    
    // 親ルート
    const rootRoute = rootRouteWithLoader({
      loader: async () => {
        // グローバルデータフェッチなども可能
      },
    })
    
    // 子ルート
    const homeRoute = new Route({
      getParentRoute: () => rootRoute,
      path: '/',
      component: HomePage,
    })
    
    const aboutRoute = new Route({
      getParentRoute: () => rootRoute,
      path: '/about',
      component: AboutPage,
    })
    
    // ルートツリーを組み立てる
    const routeTree = rootRoute.addChildren([
      homeRoute,
      aboutRoute,
    ])
    
    
  • このように、親ルート (rootRoute) に addChildren メソッドで配下ルートを配列で追加して「ツリー」を作る

  • 最終的にこの routeTreecreateRouter({ routeTree }) に渡す

Router Type Safety

  • TanStack Router は、ルーティングに関わるあらゆる場所で型安全にコードを書けるようになっている

  • 型安全にするためには、TypeScript の Declaration Merging(宣言マージ) を使って、自分が作成した router インスタンスの型を、プロジェクト全体で参照できるように登録する必要がある

  • @tanstack/react-routerのRegisterインタフェースを拡張し、ルータのインスタンスの型を持つルータプロパティを指定する

    declare module "@tanstack/react-router" {
      interface Register {
        router: ReturnType<typeof createRouter>;
      }
    }
    
  • コンポーネントで、router を取得して、存在しないパスをnavigateメソッドの引数に指定すると型エラーが出る!

    import { useRouter } from '@tanstack/react-router'
    const router = useRouter()
    
    // 存在しないパスを指定する
    router.navigate('/non-existent')
    
kaitokaito

Outlet

Outlets | TanStack Router React Docs

Outlet コンポーネント

  • ネストされたルーティング(/parent/child)において、親ルート内で子ルートのコンポーネントをレンダリングする場所を指定するために使う
  • <Outlet /> を親コンポーネントに記述することで、現在の子ルートにマッチしているコンポーネントをレンダリングしてくれる
  • Outlet コンポーネントはpropsを取らず、マッチする子ルートがない場合はnullを返す
import { createRootRoute, Outlet } from '@tanstack/react-router'

export const Route = createRootRoute({
  getParentRoute: () => rootRoute,
  path: '/parent',
  component: ParentComponent,
})

function RootComponent() {
  return (
    <div>
      <h1>My App</h1>
      <Outlet /> {/* この場所に、マッチした子ルートの中身が表示される */}
    </div>
  )
}
  • もし route の component を定義しなかった場合、そのルートは自動的に <Outlet /> をレンダリングする
const mypageRoute = createRoute({
  getParentRoute: () => rootRoute,
  path: '/mypage',
  // component が未定義なので、<Outlet /> を返す
})

const profileRoute = createRoute({
  getParentRoute: () => mypageRoute,
  path: 'profile',
  component: ProfilePage,
})

const settingsRoute = createRoute({
  getParentRoute: () => mypageRoute,
  path: 'settings',
  component: SettingsPage,
})
  • 上記の場合、/mypage は単なる「親ルート」でUIは持たず、アクセスされた時は自動で <Outlet /> として、profileやsettings の中身を差し込む役割だけになる
kaitokaito

Navigation

Navigation | TanStack Router React Docs

Everything is Relative(すべては相対的)

  • アプリ内のすべてのナビゲーションは相対パスとして処理される

  • リンクがクリックされたり、命令型のナビゲーションが呼び出されたりすると、「今どこにいるのか」と「どこへ行きたいのか」という相対的な関係でページ遷移する

  • TanStack Routerは、すべてのナビゲーションにおいて、この相対ナビゲーションの概念を常に念頭に置いているため、APIには常に2つのプロパティが表示される:

  • TanStack Router のナビゲーションAPIでは tofrom の2つのプロパティが表示される

    • from ⇒ 現在のルート
    • to ⇒ 遷移先のルート
  • from が省略された場合、ルート(/)を基準にして絶対パス解決を行う

    router.navigate({ to: 'settings' })
    
    // Router は from がないので、/settings という絶対パスに変換される
    

Shared Navigation API

  • TanStack Router で使われるナビゲーションやルートマッチングの API は、すべて共通のコアインターフェースをベースに設計されている
  • router.navigate()<Link /> などルーティングに関わるAPが同じような引数(from, to params, search など)をとるようになっている ⇒ 学習コストが低い!

To Options Interface

  • router.navigate(), router.matchRoute(), <Link /> など、すべてのルーティング操作で共通して使われるオプション型
  • 「どこから」「どこへ」「どんなパラメータやクエリをつけて」遷移・マッチングするかを全てここで指定する
項目 意味
from 現在のルート。省略した場合は /(ルート)からの絶対パス解決になる。 from: '/products'
to 遷移先のルート。絶対パスまたは、from からの相対パスを指定できる。 to: '../cart' or to: '/about'
params 動的URLパラメータ(/product/:id:id の部分)。オブジェクトまたは、前回のパラメータを引数にして返す関数として指定できる。 params: { productId: '123' }
search クエリパラメータ(?page=1 など)。オブジェクトまたは関数で渡せる。 search: { page: 2, sort: 'price' }
hash URLのハッシュ(#section1 など)。文字列または関数。 hash: 'top'
state 一時的な状態。履歴APIを使って一時的なデータを持たせる用途。関数形式でもOK。 state: { modalOpen: true }

【Tips】

  • 動的ルートを使う場合、to に直接、paramsを埋め込む(to: "product/123")はダメ

    to: '/product/$productId',  // <- このように動的パスを設定しておき、
    params: { productId: '123' }  // <- ここでパラメータを渡す
    

【Tips】

  • 全てのルートオブジェクトは to プロパティを持っている

  • この to プロパティは「そのルートに遷移する際に使用できる型安全な参照」として使える

    export const aboutRoute = createRoute({
      path: '/about',
      getParentRoute: () => rootRoute,
      component: AboutPage,
    })
    
    // ルート定義のとき、aboutRoute オブジェクトには
    // 自動的に aboutRoute.to というプロパティがつく
    
  • 遷移先のパスを手動で記述する方法

    <Link to="/about">About</Link>
    // パスが変更されると、手動で変更する必要がある
    
  • TanStackRouter での安全な書き方

    import { Route as aboutRoute } from './routes/about.tsx'
    
    function Component() {
      return <Link to={aboutRoute.to}>About</Link>
    }
    // ルート定義を変更するとこの参照も自動的に型解決される!
    
  • router.navigate()<Navigate /> でページ遷移する際に使用するオプション型
  • 基本は ToOptions を使うが、「ナビゲーション挙動」をさらに細かくカスタマイズするときには NavigateOptions を使う
プロパティ 意味
replace true にすると、履歴スタックを上書き。デフォルトは push replace: true で「戻る」ボタンで戻れない遷移になる
resetScroll 遷移後に自動的にスクロール位置を (0,0) にリセットするかどうか。 resetScroll: true で遷移後スクロールトップへ
hashScrollIntoView ハッシュ(#id)に一致する要素を自動スクロールさせるかどうか。trueScrollIntoViewOptions で詳細設定可。 hashScrollIntoView: true で遷移後に該当ID要素までスクロール
viewTransition document.startViewTransition() を使うかどうか。ブラウザが対応していれば、遷移時にスムーズなビジュアルトランジションを行う。 viewTransition: true でクロスフェードなどの滑らかな遷移が可能
ignoreBlocker ナビゲーションブロッカー(例:フォーム入力中の警告など)を無視して強制遷移するかどうか。 ignoreBlocker: true で確認ポップアップを無視して遷移
reloadDocument 通常は SPA 遷移だが、これを true にするとフルページリロードを行う。外部URL風挙動にしたい場合に使う。 reloadDocument: true でページ全体をリロード
href to の代わりに直接 href を指定できる。主に外部リンクに飛ばしたい場合に使用。 href: 'https://example.com' で外部リンクに遷移

LinkOptions Interface

  • <Link> コンポーネント専用のオプション型。
  • NavigateOptionsfrom, to, params, search, replace, resetScroll など)を継承している
  • <a> タグとしての属性や、リンク時のプリフェッチ挙動、アクティブ判定などの設定を追加できる
プロパティ 意味
target 通常の <a> タグの target 属性と同じで、_blank など指定可。 target="_blank" で新規タブで開く
preload intent を指定すると、ユーザーがホバーした時にプリフェッチ(事前読み込み)が走る。 preload: 'intent' で hover 時プリフェッチ
preloadDelay ホバーしてからプリフェッチを始めるまでの遅延時間(ms 単位)。 preloadDelay: 200
disabled true にするとリンクは無効化され、href 属性も付かなくなる。 disabled: true
activeOptions リンクがアクティブかどうか判定する条件を設定。exactincludeSearch など。 exact: true にすると完全一致のみアクティブ扱い

activeOptions の詳細

オプション 意味 よく使うケース
exact パスが完全一致したときだけアクティブにする /about/about/team が別扱いでほしい場合
includeHash URL のハッシュ部分まで含めてアクティブ判定する 特定ハッシュでセクション切り替えをする場合
includeSearch クエリパラメータを含めてアクティブ判定する /products?page=1/products?page=2 を区別したい場合
explicitUndefined searchparamsundefined でも「一致」とみなす条件を設定 通常は意識しなくてOK
  1. Link コンポーネント

    • 実際の <a> タグを生成し、href も生成してくれる
    • クリック、cmd/ctrl+クリックで新しいタブでも開ける
    • 基本的には画面上に表示する遷移ボタンやメニューリンクに使う
    • プリフェッチ機能 や アクティブ状態管理 もできる
  2. useNavigate() フック

    • フックとしてナビゲーション関数を取得できる
    • 「ユーザーの操作ではなく、副作用やイベント処理の中で遷移したい場合」に使う
    • React コンポーネント内でだけ使用できる
  3. Navigate コンポーネント

    • コンポーネントとして即時遷移を行う

    • レンダー時に自動的に遷移を実行する

    • 条件付きリダイレクトなどに便利。(例:ログインしていない時に強制遷移)

      if (!isLoggedIn) {
        return <Navigate to="/login" />
      }
      
  4. router.navigate() メソッド

    • ルーターインスタンスに直接アクセスして使う「最強・最上位」のナビゲーションAPI

    • useNavigate() と違い、React コンポーネント外でも使える

    • ユーティリティ関数やカスタムフック内でもルーターにアクセスできれば利用可能

      import { router } from './router'
      
      export function logoutAndRedirect() {
        // ログアウト処理後に強制遷移
        router.navigate({ to: '/login', replace: true })
      }
      

【Tips】

  • それぞれの使い分け
API 使いどころ
<Link> 画面に表示するナビゲーションボタンやリンク
useNavigate() イベントハンドラーや副作用内での遷移処理
<Navigate> 条件付きリダイレクトや即時遷移(コンポーネントとして使う)
router.navigate() React コンポーネント外やグローバルな場所から強制遷移

【Tips】

  • クライアントサイドのページ遷移専用
  • サーバーサイドリダイレクト(HTTPリダイレクト)が必要な場面では使えない

activeProps と inactiveProps

  • リンクが、アクティブ状態(現在のルートとマッチしているとき)は、activePropsが適用され、マッチしていないときは inactiveProps が適用される

  • 渡された styleclassName の設定は全て、マージされる

  • 静的なオブジェクトか、関数で動的に返してもOK

    <Link 
      to="/about"
      activeProps={{ className: 'text-blue-500 font-bold' }}
      inactiveProps={{ className: 'text-gray-400' }}
    >
      About
    </Link>
    

【Tips】

  • デフォルトでは to で指定したパスと現在のパスが「部分一致」した場合にアクティブ状態と判定される
  • 厳密一致させたい場合は activeOptions={{ exact: true }} を追加する

Absolute Links(絶対リンク)

<Link to="/about">About</Link>
  • 現在のルートに関係なく /about に遷移する
  • 常に同じページにリンクしたい場合に使う

Dynamic Links(動的リンク)

<Link
  to="/blog/post/$postId"
  params={{ postId: '123' }}
>
  Blog Post
</Link>
  • URL にパラメータを含むルート(例:/blog/post/:postId)に対するリンク
  • params{ postId: '...' } を渡すことで、URL の $postId が埋め込まれる

Relative Links(相対リンク)

const postIdRoute = createRoute({
  path: '/blog/post/$postId',
})

const link = (
  <Link from={postIdRoute.fullPath} to="../categories">
    Categories
  </Link>
)
  • 相対パスでリンクを作る場合は from を明示的に指定する
  • from="/blog/post/$postId" を基準に ../categories を解決すると → /blog/categories に遷移する

Search Params Links(サーチパラメータリンク)

<Link
  to="/search"
  search={{
    query: 'hoge',
  }}
>
  Search
</Link>
  • search プロパティにオブジェクトを渡すことで、クエリパラメータが付与される
  • 上記の例では、/search?query=hoge に遷移する
<Link
  to="."
  search={(prev) => ({
    ...prev,
    page: prev.page + 1,
  })}
>
  Next Page
</Link>
  • search に関数を渡すと、現在のクエリをもとに一部だけ更新できる
  • page を 1 増やしながら、他のパラメータ(例:query など)は維持

Hash Links(ハッシュリンク)

<Link
  to="/blog/post/$postId"
  params={{ postId: '123' }}
  hash="section-1"
>
  Section 1
</Link>
  • ページ内の特定のセクションにジャンプしたいときに使う
  • 上記の例だと、/blog/post/123#section-1 に遷移する
  • ブラウザはそのページ内の id="section-1" を持つ要素に自動でスクロールする

activeOptions

  • <Link> コンポーネントに渡せるオプション
  • 「このリンクはアクティブ状態かどうか?」の判定ルールを細かく制御できる
プロパティ デフォルト 説明
exact false to のパスと完全一致したときだけアクティブにする(部分一致を無効にする)
includeHash false hash(例:#section1)まで一致しているかを判定に含める
includeSearch true search(クエリパラメータ)が一致しているかを判定に含める
explicitUndefined false search のキーが undefined のとき、「そのクエリがないこと」を条件にアクティブ判定する

Passing isActive to children

  • isActive プロパティを子コンポーネントに渡して使うことができる
  • そのリンクが今アクティブかどうか を内部で自由に使って、表示内容やスタイルを変えられる
<Link to="/about">
  {({ isActive }) => (
    <span className={isActive ? 'text-blue-500' : 'text-gray-400'}>
      About
    </span>
  )}
</Link>
  • ユーザーがリンクに マウスホバー(PC)または タップ開始(スマホ)した瞬間に、そのルートに必要なコンポーネントやデータを事前に読み込む機能
  • preload="intent” をLinkコンポーネントに渡すと使える
  • preloadTimeout={100} のように設定すると、ユーザーがリンクにマウスホバーしてから、事前読み込みするまでの時間を変更することができる。(デフォルトは50ms)
<Link to="/blog/post/$postId" params={{ postId: '123' }} preload="intent">
  Blog Post
</Link>

useNavigateフック

  • useNavigate() はナビゲーション用の関数を返すフック

  • 副作用の中で動的に遷移する場合に使う

    ⇒ 処理やフォーム送信の成功後など、「ロジックの中で遷移したいとき」

  • リンクやボタンなど、ユーザーの操作によってユーザーが操作できる場合は、Linkコンポーネントを使う!

function Component() {
  const navigate = useNavigate({ from: '/posts/$postId' })

  const handleSubmit = async (e: FrameworkFormEvent) => {
    e.preventDefault()

    const response = await fetch('/posts', {
      method: 'POST',
      body: JSON.stringify({ title: 'My First Post' }),
    })

    const { id: postId } = await response.json()

    if (response.ok) {
      navigate({ to: '/posts/$postId', params: { postId } })
    }
  }
}
  • useNavigate({ from: '/posts/$postId' })
    ⇒ ここで、現在のルートを指定しておくことで、navigate()を呼び出すたびに from を渡す必要がなくなる

  • navigate({ to, params })

    ⇒ to は遷移先、params はURLパラメータを埋め込む

  • Navigayeコンポーネントを使うと、コンポーネントがマウントされたときに即時にページ遷移させることができる
<Navigate to="/posts/$postId" params={{ postId: 'my-first-post' }} />

router.navigate() 関数

router.navigate() は、useNavigate() の代わりに使えるナビゲーション関数

  • Reactコンポーネント外でも使える
  • 渡すことのできる props は useNavigate() と同じ
// router.ts で作成したインスタンスをインポートする
import { router } from '@/app/router'

export function logoutAndRedirect() {
  // ログアウト処理
  localStorage.removeItem('token')

  // ログインページへリダイレクト
  router.navigate({ to: '/login', replace: true })
}

useMatchRoute フック と MatchRoute コンポーネント

  • useMatchRoute フックと <MatchRoute> コンポーネントを使うと、「今そのルートにマッチしているか?」を判定することができる
  • useMatchRoute フックは、ロジック内で判定するときに使う
  • <MatchRoute> コンポーネントは、JSX要素内で判定するときに使う
  • ToOptions インターフェースに対応している(to, params, search, hash, from など)

pending オプション

  • ルートがあるページに遷移中である場合に、true を返す

  • ページ切り替え時のスピナーの表示に使うことができる

    • JSX要素をそのまま渡す

      ⇒ スピナーを表示するかしないかだけを切り替えるときに使う

    <MatchRoute to="/users" pending>
      <Spinner />
    </MatchRoute>
    
    • 関数として、children を渡す

      ⇒ 表示方法やスタイルも動的に変えたい場合に使う

    <MatchRoute to="/users" pending>
      {(match) => <Spinner show={match} />}
    </MatchRoute>
    
    • hookをを使う
      ⇒ イベントハンドラ・副作用(useEffect)・条件分岐などで使う
      ⇒ UIの表示/非表示ではなく、副作用を実行するのに適している
    useEffect(() => {
      if (matchRoute({ to: '/users', pending: true })) {
        console.info('The /users route is matched and pending')
      }
    }, [])
    
kaitokaito

Path Params(動的ルート)

Path Params | TanStack Router React Docs

基本的な使い方

  • Path Params は URLの中で動的に変わる部分を変数として扱える仕組み
  • $変数名 の部分が「変数」として抽出され、ルートの loader, component, などからアクセスできる
export const Route = createFileRoute('/posts/$postId')({
  loader: async ({ params }) => {
    return fetchPost(params.postId)
  },
})
  • 上記の場合、/posts/abc123 にアクセスされたとき、params.postId === 'abc123' になる

Path Params は子ルートで使う

  • パスパラメータは / で区切られる「1つの区切り」単位でしかマッチしないが、子ルートを定義すれば、使うことができる

    '/posts/$postId'         ← 親ルート
    '/posts/$postId/edit'    ← 子ルート
    '/posts/$postId/comments' ← 子ルート
    
  • params.postId は 親ルートの $postId から取得された値だが、子ルートである /edit/comments などでは そのまま利用できる

    export const Route = createFileRoute('/posts/$postId')({
      component: PostLayout,
    })
    
    export const Route = createFileRoute('/posts/$postId/edit')({
      loader: ({ params }) => {
        return fetchPostForEdit(params.postId) // 親のパラメータを普通に使える!
      },
    })
    

loaderbeforeLoad と合わせて使う

  • Path Params は、params オブジェクトとして、loader や beforeLoad に渡される
  • /blog/123 にアクセスすると、{ postId: ‘123’ } のようなオブジェクトが渡される
export const Route = createFileRoute('/posts/$postId')({
  beforeLoad: async ({ params }) => {
    // do something with params.postId
    console.log('今アクセスされようとしている記事ID:', params.postId)
  },
})

コンポーネント内で Path Params を取得する

  • コンポーネント内で Path Params を取得するには、 useParams フックを使う必要がある
export const Route = createFileRoute('/posts/$postId')({
  component: PostComponent,
})

function PostComponent() {
  const { postId } = Route.useParams()
  return <div>Post {postId}</div>
}
  • Route.useParams() で、そのルートにマッチしたパスパラメータが取得できる

  • useParams() の戻り値はルート定義に基づいた型安全なオブジェクトとなる

    $postId と定義したら、useParams() の戻り値に postId が含まれることを TypeScript が認識してくれる

【Tips】

  • コード分割してコンポーネントを遅延読み込みするとき、Route(ルートの定義オブジェクト)を直接 import したくない

    Route.useParams() を使うために import してしまうと、ルート定義ファイルも一緒にロードされるため、コード分割の意味がなくなってしまう

  • getRouteApi() を使うと、Route を直接 import する必要がないので、コード分割することができる

    import { getRouteApi } from '@tanstack/react-router'
    
    const { useParams } = getRouteApi('/posts/$postId')
    
    function PostPage() {
      const { postId } = useParams()
      return <div>Post {postId}</div>
    }
    

Route に属していないコンポーネントで Path Params を使う

  • Route.useParams() は Route にマッチしているコンポーネント内でしか使えない
  • ルート外のコンポーネント(レイアウトコンポーネント、ヘッダー、フッターなど)では、グローバルな useParams() を使うことで取得できる
import { useParams } from '@tanstack/react-router'

function PostComponent() {
  const { postId } = useParams({ strict: false })
  return <div>Post {postId}</div>
}
  • strict: false を指定することで、「ルートに紐づかなくても path param を取得する」ことを明示する
  • 現在のルートツリー全体から、params を探す

Path Params 付きのルートへ遷移する

  • $postId のように動的ルート(Path Params)が含まれている場合、ページ遷移するときに params を必ず指定する必要がある

例:オブジェクトで指定する

<Link to="/blog/$postId" params={{ postId: '123' }}>
  Post 123
</Link>

例:関数で指定する

<Link to="/blog/$postId" params={(prev) => ({ ...prev, postId: '123' })}>
  Post 123
</Link>
  • params に「関数」を渡すことで、現在のパスパラメータ prev を元に新しいパラメータを生成
  • prev は現在の URL に存在している path params
  • 一部のパラメータだけを上書きして、他のパラメータはそのまま維持したいときに便利。
kaitokaito

Search Params

Search Params | TanStack Router React Docs

なぜ URL Search Params をそのまま使わないのか?

  • 従来のSearch Param APIはいくつかのことを前提としているため扱いづらい…

    1. Search Params が文字列であること

      • 数値として扱いたいときは、Number()parseInt() を使って変換が必要
    2. フラットなデータが扱えない

      • 配列やオブジェクトを直接扱えない
      • ネストされた状態(例: filters={ category: 'space', price: [100, 200] })を表現しづらい
    3. シリアライズ/デシリアライズが貧弱

      • URLSearchParams は文字列との相互変換(toString()get())に頼るだけ。
      • カスタムな変換処理(JSON形式など)を挟めない。
      • 例:?filters=%7B%22category%22%3A%22space%22%7D ← 読みにくくメンテしにくい。
    4. 変更の粒度が粗い

      • URLSearchParams を使って search params を更新すると、常に URL の pathname も一緒に扱わなければならない
      window.history.pushState({}, '', `/products?page=2`) // パスごと変更
      
  • TanStackRouter でどのように改善するか…

    課題 TanStack Router の解決策
    string にしか対応していない パラメータごとに型定義できる(boolean, number, string など)
    フラットな構造しか扱えない ネストしたオブジェクトや配列にも対応
    カスタムシリアライズ不可 parseSearch, stringifySearch で自由に定義できる
    リアクティブに扱えない useSearch(), useNavigate() で search を状態のように扱える
    無駄な再パースでパフォーマンス低下 Structural Sharing を考慮して参照の整合性を保つ
    pathname と search が強く結びついている pathname に影響を与えず search だけ変更できる(navigate({ to: ".", search })

Search Params は、元祖グローバル状態管理だ!

ユーザー視点

  • 状態がURLにあるからこそ、ユーザー体験が保たれる
ユーザー行動 なぜ Search Params が重要?
Cmd/Ctrl + クリック タブを開いても、ちゃんとフィルターやページ番号が保持されているべき
ブックマークやURL共有 他人が開いたときにも同じ状態でページが表示されるべき
リロード・戻る・進む UI状態が消えないように、URLに状態を保つべき

開発者視点

開発者のニーズ TanStack Routerが解決するポイント
簡単にURLの状態を書き換えたい useSearch(), navigate() でできる
値の型を明示したい search schema によって boolean, number もOK
シリアライズに悩みたくない parseSearch, stringifySearch が自動対応

JSON ファーストな Search Params

ポイント

  • TanStackRouter では、Search Parms を文字列ではなく、JSONデータとして扱えるようにしている
特徴 説明
自動で数値として扱える "3"3 に自動変換される
boolean型も扱える "true"true に自動変換される
配列・オブジェクトも扱える JSON 形式で encode/decode される
双方向に変換 URL → JSON → URL の変換が自動かつ安全
他のツールとの互換性 第一階層は通常のURLと同じ key?=value 形式のため、互換性が保たれる

実用例

  • 通常の React の state オブジェクトのように、型付きの構造化データをそのまま渡す

    <Link
      to="/shop"
      search={{
        pageIndex: 3,
        includeCategories: ['electronics', 'gifts'],
        sortBy: 'price',
        desc: true,
      }}
    />
    
  • 生成されるURL

    /shop?pageIndex=3
          &includeCategories=%5B%22electronics%22%2C%22gifts%22%5D
          &sortBy=price
          &desc=true
    
  • URLをパースして取得できる値

    {
      "pageIndex": 3,
      "includeCategories": ["electronics", "gifts"],
      "sortBy": "price",
      "desc": true
    }
    

Search Parms の型付けとバリデーション

何故バリデーションが必要なのか?

  • Serach Params はユーザーが任意に変更できるので、そのまま使うとクラッシュしたり、予期せぬ挙動を引き起こすことになる

    /shop/products?page=aaa&sort=hacked
    
    page → "aaa"(数値じゃない)
    sort → "hacked"(存在しない値)
    

ValidateSearch オプション

  • Route の validateSearch オプションを使うことで、各ルートで Search Params を検証することができる
export const Route = createFileRoute('/shop/products')({
  validateSearch: (search: Record<string, unknown>): ProductSearch => {
    return {
      page: Number(search?.page ?? 1),
      filter: (search.filter as string) || '',
      sort: (search.sort as ProductSearchSortOptions) || 'newest',
    }
  },
})
kaitokaito

Authenticated Routes(認証ルート)

Authenticated Routes | TanStack Router React Docs

概要

  • Authenticated Routes を使うと、ログインしていないときに表示させたくないパスを設定したり、ログインページにリダイレクトさせることができる

route.beforeLoad オプション

  • router.beforeLoadオプションを使うと、ルートが読み込まれる前に実行される関数を指定することができる
  • イメージとしては、「このページを表示してもいいか?」を判断する門番さん的な存在

ページが読み込まれるまでの処理順

  1. Route Matching(ルートマッチング)
    • パラメータを解析 route.params.parse
    • クエリを検証 route.validateSearch
  2. Route Loading(ルートロード前の処理)
    • 認証チェックroute.beforeLoad
    • エラーハンドリング route.onError
  3. Route Loading(非同期並列処理)
    • コンポーネントの事前ロード route.component.preload
    • データフェッチや初期化処理 route.load

例:/mypage というルートを表示しようとした場合

1. URL確認(/mypage)
2. パラメータ検証 (params.parse, validateSearch)
3. beforeLoad 実行 → ここで「ログイン済み?」を確認  
   - 未ログインなら /login にリダイレクト
   - ログイン済みなら次の段階へ
4. コンポーネントの preload 実行(必要に応じてデータ先読み)
5. 最終的にコンポーネントをロードして描画

ポイント

  • コンポーネントが読み込まれるより前に、beforeload(認証処理)をすることで、認証が失敗したときも安全
  • 親ルートの beforeLoad ⇒ 子ルートの breforeLoad の順で実行されるため、親ルートでログイン確認をして、子ルートで権限チェックをするといった使い分けができる

Redirecting(リダイレクト)

  • 必須ではないが、認証フローによってはログインページにリダイレクトさせる必要がある
  • リダイレクトさせるには、beforeLoad() の中で redirect() をスローする

認証ガード

export const Route = createFileRoute('/_authenticated')({
  beforeLoad: async ({ location }) => {
    if (!isAuthenticated()) {
      throw redirect({
        to: '/login',
        search: {
          redirect: location.href,
        },
      });
    }
  },
});
  • beforeLoad()の中で、認証されているかチェックする関数を呼び出し、認証されていなければ、throw redirect()を使って、/loginに強制遷移させている
  • search param に location.href を含めることで、ログイン成功後に元々アクセスしようとしていたページに戻すことができる
    • ユーザーが /mypage にアクセス
    • mypage でログインしてないことを検知
    • /login?redirect=https://example.com/login に飛ばす
    • ログイン成功したら search.redirect に入っている URL へ戻す

ログインハンドラー

const handleLogin = async () => {
  // ログイン処理
  await login();

  // URL から redirect パラメータ取得
  const search = router.state.location.search;
  if (search.redirect) {
    router.history.push(search.redirect);
  } else {
    router.history.push('/'); // デフォルトはトップへ
  }
};
  • 正常にログインできた場合、router.history.push(search.redirect); を呼び出す
  • これにより、ユーザーが戻るボタンを押したときに、ログインページに戻るのを防ぐことができる

Non-Redirected Authentication(非リダイレクト認証)

  • ログインページにリダイレクトさせずに、同じページ上でログインフォームを表示させることができる
    • SPAで画面遷移せずに、モーダルでログインフォームを表示させる
    • ログインが完了したら、そのままコンテンツに切り替わる
export const Route = createFileRoute('/_authenticated')({
  component: () => {
    if (!isAuthenticated()) {
      return <Login />   // 未認証なら Login コンポーネントを表示
    }

    return <Outlet />    // 認証済みなら <Outlet /> で子ルートを描画
  },
})

ポイント

  • isAuthenticated() でログイン済みかチェックし、ログインしていなければ子ルートはレンダリングせずにログインフォームを表示する
  • ログイン済みであれば、Outlet で子ルートをレンダリングする

メリット

  • URLが変わらないので、「ログイン後にどこに戻すか?」のロジックが不要
  • ページ遷移によるチラつきがない

React context/hooks を使った認証

  • 認証フローがReactの context や カスタムhook に依存している場合、router.context オプションを使用して、認証状態を TanStackRouter に渡す必要がある

なぜ、router.contextが必要なのか?

  • React の hooks はコンポーネントの中でしか呼べない
  • beoreLoad で認証状態を使いたい場合、コンポーネント内で hook から取り出した状態を router.context に渡す必要がある

認証ルートを保護するために context と hook を使用する例

  1. MyRouterContext を定義し、root route をこの context 型付きで定義

    import { createRootRouteWithContext } from '@tanstack/react-router'
    
    interface MyRouterContext {
      // useAuthフックのReturnTypeまたはAuthContextの値
      auth: AuthState
    }
    
    export const Route = createRootRouteWithContext<MyRouterContext>()({
      component: () => <Outlet />,
    })
    
  2. routerを作成

    import { createRouter } from '@tanstack/react-router'
    
    import { routeTree } from './routeTree.gen'
    
    export const router = createRouter({
      routeTree,
      context: {
        // 初期値はダミーでOK。実際はコンポーネント内で渡す。
        auth: undefined!,
      },
    })
    
  3. App.tsxuseAuth() から取得した情報を router.context に渡す

    import { RouterProvider } from '@tanstack/react-router'
    
    import { AuthProvider, useAuth } from './auth'
    
    import { router } from './router'
    
    function InnerApp() {
      const auth = useAuth()
      return <RouterProvider router={router} context={{ auth }} />
    }
    
    function App() {
      return (
        <AuthProvider>
          <InnerApp />
        </AuthProvider>
      )
    }
    

    ポイント

    • React コンポーネント内で useAuth() を使って認証状態を取得
    • 取得した状態を router.context に注入
    • これにより、boreLoad の中で context.auth として安全に認証状態が使えるようになる
  4. 認証ガードを実装する

    import { createFileRoute, Outlet, redirect } from "@tanstack/react-router";
    
    export const Route = createFileRoute("/mypage")({
      beforeLoad: ({ context, location }) => {
        if (!context.userState.isLoggedIn) {
          throw redirect({
            to: "/login",
            search: { redirect: location.href },
          });
        }
      },
      component: RouteComponent,
    });
    
    function RouteComponent() {
      return <Outlet />;
    }
    

    ポイント

    • context.auth.isAuthenticated を使って認証チェック
    • 認証されていない場合は /login へリダイレクト
    • redirect: location.href で、もともと遷移しようとしていたパスをクエリパラメータで渡す
  5. /login クエリパラメータを設定する

    import { createFileRoute, redirect } from "@tanstack/react-router";
    import LoginPage from "@/components/templates/LoginPage/LoginPage";
    
    type loginSearch = {
      redirect?: string;
    };
    
    export const Route = createFileRoute("/login")({
      // クエリパラメータのバリデーション
      validateSearch: (search: Record<string, unknown>): loginSearch => {
        return {
          redirect: search.redirect as string,
        };
      },
      component: RouteComponent,
    });
    
    function RouteComponent() {
      return <LoginPage />;
    }
    
    • validateSearch を使って、URLで渡ってきたクエリパラメータ(?redirect=/mypage)を型チェック&バリデーションチェックする
    • 成功した場合は型付きオブジェクトとして route.loader や beforeLoad などで安全に参照可能にする
    • 失敗した場合は自動的にエラールートに遷移してくれる
kaitokaito

404ページの設定

Not Found Errors | TanStack Router React Docs
Creating a Router | TanStack Router React Docs

notFoundMode

存在しないページにアクセスしたときに、404ページを表示する設定はnotFoundModeで挙動が変わる。

fuzzy

  • デフォルトでは、fuzzyが設定される
  • 最も近い親レイアウト(src/route.tsx)を維持しつつ、その親レイアウトに設定したnotFoundComponentを表示する
  • 親ページのナビゲーションや UI を維持したまま「ページが見つからない」という情報を表示したい時に使う

root

  • 存在しないページにアクセスしたときは、src/routes/__root.tsxに設定したnotFoundComponentを表示する
  • 全ての404ページを統一したいときに使う

手順

  • configファイルにnotFoundModeオプションを追加する
import { createRouter as createTanStackRouter } from "@tanstack/react-router";
import { routeTree } from "./routeTree.gen";

export function createRouter() {
  const router = createTanStackRouter({
    routeTree,
    notFoundMode: "root", // notFoundModeの設定
  });

  return router;
}

declare module "@tanstack/react-router" {
  interface Register {
    router: ReturnType<typeof createRouter>;
  }
}
  • 親ルートにnotFoundComponentを設定する

ルートrootに設定する場合

⇒ 存在しないページにアクセスした時は、Headerコンポーネント、Footerコンポーネント、NotFoundコンポーネントがレンダリングされる

import { createRootRoute, Link, Outlet } from "@tanstack/react-router";
import Header from "@/components/templates/Header/Header";
import Footer from "@/components/templates/Footer/Footer";
import NotFound from "@/components/templates/NotFound/NotFound";

export const Route = createRootRoute({
  component: () => (
    <>
      <Header />
      <Outlet />
      <Footer />
    </>
  ),
  notFoundComponent: () => <NotFound />,
});

任意のルートに設定する場合

store/non-existent-pageのように/store以下のルートで存在しないページにアクセスしたときは、ルートrouteに設定しているコンポーネントと、MenubarコンポーネントとNotFoundコンポーネントが一緒にレンダリングされる

import { createFileRoute, Outlet } from "@tanstack/react-router";
import Menubar from "@/components/organisms/Menubar/Menubar";
import NotFound from "@/components/templates/NotFound/NotFound";

export const Route = createFileRoute("/store")({
  component: RouteComponent,
  notFoundComponent: () => <NotFound />,
});

function RouteComponent() {
  return (
    <>
      <Menubar />
      <Outlet />
    </>
  );
}