TanStack Routerで実現する高度なクエリパラメータ管理

に公開

はじめに

ReactアプリケーションでURLクエリパラメータを扱う際に直面する課題として、型安全性の欠如、複雑なデータ構造の扱いづらさ、デフォルト値の管理、そしてパフォーマンスの最適化などがあります。

既存のルーティングライブラリ(React Router、Next.js、Expo Routerなど)は基本的なuseSearchParamsuseLocalSearchParamsフックを提供していますが、これらは手動での型変換、文字列処理、バリデーションが必要で、開発者体験を大きく損なっています。

本記事では、TanStack Routerが提供する高度なクエリパラメータ管理機能、そしてURLクエリパラメータを真の状態管理として活用する方法を紹介します。

TanStack Routerのクエリパラメータ機能の特徴

TanStack Routerは、Tanner Linsleyによって開発された次世代ルーターです。URLクエリパラメータ管理の文脈では、Search Params(検索パラメータ)を第一級の概念として扱います。

従来のルーティングシステムでは、検索パラメータは「おまけ」として扱われ、手動での文字列解析や型安全性の欠如に悩まされてきました。TanStack Routerは、この問題に正面から取り組み、検索パラメータをルート定義の中核に配置して解決します。

Search Paramsへの型安全なアプローチ

TanStack Routerの最大の特徴は、URLクエリパラメータに対する徹底した型安全性です:

"One of the most overlooked but powerful tools in web development is the URL. It's the original state‑management system — fast, shareable, and intuitive."

見落とされがちですが、ウェブ開発で最も強力なツールの一つがURLです。高速・共有可能・直感的という「元祖ステート管理」。

Tanner Linsley, TanStack Blog

さらに、Search Paramsを真の状態として扱うことの重要性について:

"When you bring validation, typing, and ownership into the router itself, you stop treating URLs like strings and start treating them like real state"

バリデーション、型付け、所有権をルーター自体に組み込むことで、URLを単なる文字列ではなく、真の状態として扱うようになります。

Search Params Are State - TanStack Blog

この哲学を具現化するため、TanStack Routerは以下の機能を提供します:

  • Zodスキーマによる実行時バリデーション: URLパラメータの型と値を厳密に検証
  • JSON-firstアプローチ: 複雑なオブジェクトや配列を自然に扱える
  • Search Middleware: URLの正規化とデフォルト値の管理を自動化
  • 細粒度サブスクリプション: 特定のパラメータのみを監視し、不要な再レンダリングを防ぐ

これらの機能により、TanStack Routerは、URLクエリパラメータを構造化されたデータストアとして扱うことで、より堅牢で保守性の高いクエリ管理を実現します。

実践例

設計哲学とJSON-first Search Params

TanStack Routerの特徴的な点は、URLを第一級のデータストアとして扱うことです。従来のルーターがURLを「文字列の集まり」として扱うのに対し、TanStack RouterはURLを「構造化されたJSONデータの永続化層」として捉えます。

TanStack Routerは、ネストしたデータ構造をJSONとして自動的にエンコードします:

// 従来:文字列のみ
?page=1&sort=price&order=asc

// TanStack Router:構造化データ
// ネストしたオブジェクトはJSONとしてエンコードされる
?page=1&filters=%7B%22category%22%3A%22electronics%22%2C%22priceRange%22%3A%7B%22min%22%3A100%2C%22max%22%3A1000%7D%7D
// デコード後: filters={"category":"electronics","priceRange":{"min":100,"max":1000}}

この設計により、シンプルな値と複雑なデータ構造の両方を扱えるようになっています。詳細は公式ドキュメントをご覧ください。

Zodによる型安全なバリデーション

TanStack Routerは、URLクエリパラメータのバリデーションにスキーマライブラリを使用することで、より高度な型安全性を実現しています。

スキーマライブラリを使用することで、以下のようなメリットがあります:

  1. 単一の情報源(Single Source of Truth): スキーマから型とバリデーションの両方を生成
  2. エラーハンドリング: .catch()で不正な値に対するフォールバック
  3. 変換機能: 文字列から数値への自動変換など
  4. 合成可能性: スキーマを組み合わせて複雑な構造を表現

他のバリデーションライブラリもサポート

今回はZodを使用しますが、TanStack RouterはZod以外にも、Valibot、Yup、ArkType、Effect/Schemaなどのバリデーションライブラリをサポートしています。

実装例と型推論の仕組み

import { createFileRoute } from '@tanstack/react-router'
import { z } from 'zod'

// Zodでスキーマを定義
const productSearchSchema = z.object({
  page: z.number().catch(1),           // 数値型、エラー時は1
  filter: z.string().optional(),       // オプショナル文字列
  sort: z.enum(['newest', 'oldest', 'price']).catch('newest'),

  // ネストしたオブジェクト
  priceRange: z.object({
    min: z.number().default(0),
    max: z.number().default(1000)
  }).optional(),

  // 配列
  categories: z.array(z.string()).default([]),

  // 複雑な条件
  availability: z.object({
    inStock: z.boolean().default(false),
    shipping: z.enum(['all', 'free', 'express']).default('all')
  })
})

export const Route = createFileRoute('/products')({
  validateSearch: productSearchSchema,
  component: ProductList
})

function ProductList() {
  // 完全に型付けされたsearch
  const search = Route.useSearch()
  // search.page: number
  // search.filter: string | undefined
  // search.priceRange: { min: number, max: number } | undefined

  return <div>{/* 商品リスト */}</div>
}

クエリパラメータ更新と型推論の仕組み

TanStack Routerの型推論は、ルート定義時のスキーマから自動的に生成され、すべてのAPI(Link、useNavigate、useSearch)で一貫した型安全性を提供します:

import { createFileRoute, Link, useNavigate } from '@tanstack/react-router'
import { z } from 'zod'

// Zodスキーマでクエリパラメータを定義
const productSearchSchema = z.object({
  page: z.number().catch(1),
  sort: z.enum(['newest', 'oldest', 'price']).catch('newest'),
  categories: z.array(z.string()).optional(),
  priceRange: z.object({
    min: z.number(),
    max: z.number()
  }).optional()
})

// ルート定義でスキーマを適用
export const Route = createFileRoute('/products')({
  validateSearch: productSearchSchema,
  component: Products
})

function Products() {
  // useSearchは完全に型付けされる
  const search = Route.useSearch()
  // search.page: number
  // search.sort: 'newest' | 'oldest' | 'price'

  const navigate = useNavigate({ from: Route.fullPath })

  return (
    <div>
      {/* Linkコンポーネントでも型推論が効く */}
      <Link
        to="."
        search={(prev) => ({
          ...prev,
          page: prev.page + 1  // ✅ pageはnumber型として推論
        })}
      >
        次のページ
      </Link>

      {/* TypeScriptが不正な値を検出 */}
      <Link
        to="."
        search={{
          page: "1",        // ❌ Type error: string は number に代入不可
          sort: 'invalid'   // ❌ Type error: 'invalid' は許可された値ではない
        }}
      >
        エラーになるリンク
      </Link>

      {/* 関数形式で複雑な更新も型安全 */}
      <button
        onClick={() => {
          navigate({
            search: (prev) => ({
              ...prev,
              categories: prev.categories?.includes('electronics')
                ? prev.categories.filter(c => c !== 'electronics')
                : [...(prev.categories || []), 'electronics'],
              page: 1  // カテゴリ変更時はページをリセット
            })
          })
        }}
      >
        電化製品でフィルター
      </button>
    </div>
  )
}

Search Middleware による正規化

Search Middlewareは、TanStack Routerの機能の1つで、URLの更新前後に処理を挟むことができます。TanStack Routerはいくつかの組み込みミドルウェアを提供するとともに、開発者がカスタムミドルウェアを実装できるようにしています。

なぜSearch Middlewareが必要なのか?

実際のアプリケーションでは、以下のような課題に直面します:

  1. URLの肥大化: デフォルト値が常にURLに含まれる(?page=1&sort=newest&view=grid
  2. グローバルパラメータの消失: ページ遷移時に言語設定やテーマが失われる
  3. 不正な値の混入: 相互に矛盾するパラメータの組み合わせ
  4. 一貫性の欠如: 同じ状態でも異なるURL表現が生まれる

組み込みミドルウェア機能

1. stripSearchParams - デフォルト値の自動削除

stripSearchParamsは、URLを簡潔に保つための重要な機能です。デフォルト値と同じ値を持つパラメータを自動的にURLから削除し、URLの可読性を向上させます。

import { stripSearchParams } from '@tanstack/react-router'

export const Route = createFileRoute('/products')({
  validateSearch: productSearchSchema,
  search: {
    middlewares: [
      // デフォルト値と一致する場合、URLから自動削除
      stripSearchParams({
        page: 1,        // ?page=1 → 削除
        sort: 'newest', // ?sort=newest → 削除
        view: 'grid',   // ?view=grid → 削除
      })
    ]
  }
})

// 結果:
// /products?page=1&sort=newest → /products
// /products?page=2&sort=newest → /products?page=2
// /products?page=1&sort=oldest → /products?sort=oldest

動的なデフォルト値の設定も可能です:

stripSearchParams((search) => ({
  // ユーザーの設定に基づいて動的にデフォルト値を決定
  theme: getUserPreference('theme') || 'light',
  lang: getUserLocale() || 'en',
  // 条件付きデフォルト値
  sort: search.category === 'new' ? 'date' : 'relevance'
}))
2. retainSearchParams - 特定パラメータの永続化

retainSearchParamsは、ページ遷移時に特定のクエリパラメータを保持するための機能です。グローバルな設定(テーマ、言語など)やセッション情報を維持する際に非常に有用です。

import { retainSearchParams } from '@tanstack/react-router'

// ルートレベルでグローバルパラメータを保持
export const Route = createRootRoute({
  search: {
    middlewares: [
      // 特定のパラメータを常に保持
      retainSearchParams(['theme', 'lang']),
    ]
  }
})

// 結果:
// /products?theme=dark&page=2 から /categories へ遷移
// → /categories?theme=dark&lang=en
// (theme, langが保持され、pageは破棄される)

カスタムミドルウェアの実装例

上記の組み込みミドルウェアに加えて、開発者は独自のミドルウェアを実装できます:

// 検索クエリの正規化ミドルウェア
const normalizeSearchMiddleware = ({ search, next }) => {
  // 空白文字の正規化
  if (search.q) {
    search.q = search.q.trim().toLowerCase()
    // 空文字の場合は削除
    if (!search.q) delete search.q
  }

  // 価格範囲の妥当性チェック
  if (search.minPrice && search.maxPrice) {
    if (search.minPrice > search.maxPrice) {
      // 自動的に入れ替え
      [search.minPrice, search.maxPrice] = [search.maxPrice, search.minPrice]
    }
  }

  // ページネーションのリセット
  if (search.filters || search.q) {
    // フィルターや検索が変更されたらページを1にリセット
    search.page = 1
  }

  return next(search)
}

// 複数のミドルウェアを組み合わせ
export const Route = createFileRoute('/products')({
  validateSearch: productSearchSchema,
  search: {
    middlewares: [
      normalizeSearchMiddleware,
      stripSearchParams({ page: 1, sort: 'newest' }),
      retainSearchParams(['theme', 'lang'])
    ]
  }
})

このミドルウェアシステムにより、URLは常に最小限かつ一貫性のある形式に保たれ、ユーザビリティとSEOの両方が向上します。

細粒度サブスクリプション

パフォーマンスの課題

大規模なアプリケーションでは、URLに多数のクエリパラメータが含まれることがよくあります。このとき、たとえば1つのパラメータ(例: ページ番号)が変わるたびに、すべてのコンポーネントが再レンダリングされてしまうと、パフォーマンスが大きく低下してしまいます。

TanStack Routerは、Reactの一般的なパフォーマンス最適化パターンをURLクエリパラメータ管理に適用し、細粒度サブスクリプションを可能にしています。これにより、コンポーネントはURLの中で必要な部分だけを監視できるようになります。

実装パターンとベストプラクティス

// ❌ 悪い例:URL全体の変更で再レンダリング
function ProductTable() {
  const search = Route.useSearch() // すべての検索パラメータにサブスクライブ

  // pageが変更されるだけで、このコンポーネント全体が再レンダリング
  return <ExpensiveTable filters={search.filters} />
}

// ✅ 良い例:必要な部分だけを選択
function OptimizedProductTable() {
  // filtersのみにサブスクライブ
  const filters = Route.useSearch({
    select: (search) => search.filters
  })

  // filtersが変更されたときのみ再レンダリング
  return <ExpensiveTable filters={filters} />
}

// 複数の値を選択して構造化
function ProductHeader() {
  const sortConfig = Route.useSearch({
    select: (search) => ({
      sortBy: search.sort,
      sortOrder: search.order
    }),
    // structuralSharing: 構造的な等価性チェックを有効化
    structuralSharing: true
  })

  return <SortControls {...sortConfig} />
}

Structural Sharingとは?

structuralSharingは、TanStack Routerが提供する最適化機能の一つです。通常、JavaScriptでは新しいオブジェクトを作成すると、たとえ内容が同じでも異なる参照として扱われ、Reactは再レンダリングを行います。

// structuralSharingなしの場合
const config1 = { sortBy: 'name', order: 'asc' }
const config2 = { sortBy: 'name', order: 'asc' }
console.log(config1 === config2) // false(異なる参照)

// structuralSharingありの場合
// 内容が同じなら同じ参照を返す
console.log(sharedConfig1 === sharedConfig2) // true(同じ参照)

structuralSharing: trueを設定すると、TanStack Routerは:

  1. 新しく生成されたオブジェクトと前回のオブジェクトを深く比較
  2. 内容が同じ場合は前回の参照をそのまま返す
  3. 不要な再レンダリングを防止

これにより、特に複雑なオブジェクトを返すセレクタで大きなパフォーマンス向上が期待できます。

高頻度入力への対応:カスタムフックによる解決策

入力遅延問題の根本原因

検索ボックスやフィルターなど、ユーザーが頻繁に入力を行うUIコンポーネントでは、URLクエリパラメータとの同期において特有の課題が発生します。

URLSearchParamsを使った素朴な実装では、以下のような処理フローで遅延が発生します:

ユーザー入力 → URL更新 → ブラウザ履歴更新 → 再レンダリング → UI更新
                    ↑                     ↑
              体感できる遅延が発生      追加の遅延

この遅延により、ユーザーは「入力が反映されない」と感じ、UXが大幅に低下します。Google の研究によると、100ms以上の遅延はユーザーが知覚し、1秒以上の遅延は思考の流れを中断させるとされています。

さらに問題なのは、この遅延が累積的であることです。ユーザーが連続して文字を入力する場合、各入力がキューに積まれ、体感的な遅延はさらに悪化します。

解決アプローチ:カスタムフックの実装

高頻度入力に対する即座のUI反応を実現するには、開発者自身がカスタムフックを実装する必要があります。TanStack Routerはこのようなパターンを直接提供していませんが、その型安全なAPIを活用して、以下のようなローカル状態とURLの分離パターンを実装できます:

function useResponsiveQueryParam<T>(
  paramName: string,
  defaultValue: T
): [T, (value: T) => void, boolean] {
  const navigate = useNavigate({ from: Route.fullPath })
  const search = Route.useSearch()

  // URLの値
  const urlValue = search[paramName] ?? defaultValue

  // ローカル状態(即座の反応用)
  const [localValue, setLocalValue] = useState(urlValue)
  const [isPending, startTransition] = useTransition()

  // URL変更をローカル状態に反映
  useEffect(() => {
    if (!isPending) {
      setLocalValue(urlValue)
    }
  }, [urlValue, isPending])

  // 更新関数
  const updateValue = useCallback((newValue: T) => {
    // 1. 即座にローカル状態を更新(UIが即反応)
    setLocalValue(newValue)

    // 2. URLを非同期で更新
    startTransition(() => {
      navigate({
        search: (prev) => ({
          ...prev,
          [paramName]: newValue === defaultValue ? undefined : newValue
        }),
        replace: true
      })
    })
  }, [navigate, paramName, defaultValue])

  return [localValue, updateValue, isPending]
}

// 使用例:Zodスキーマと組み合わせて使用
const searchSchema = z.object({
  q: z.string().catch(''),
  page: z.number().catch(1)
})

function SearchBox() {
  // TanStack Routerの型安全なAPIを活用
  const search = Route.useSearch()
  // カスタムフックで即座の反応を実現
  const [query, setQuery, isPending] = useResponsiveQueryParam('q', search.q)

  return (
    <input
      value={query}
      onChange={(e) => setQuery(e.target.value)}
      placeholder="検索..."
      className={isPending ? 'opacity-70' : ''}
    />
  )
}

このカスタムフックにより:

  • 即座のUI反応: ローカル状態の更新によりユーザー入力が即座に反映
  • 非同期URL更新: React TransitionによりURLの更新が非ブロッキングで実行
  • 一貫性の保証: URLとUIの状態が最終的に同期される

TanStack Routerの型安全性、Search Middleware、細粒度サブスクリプションと組み合わせることで、高頻度入力UIでも優れたユーザー体験を実現できます。

まとめ

TanStack Routerはルーティングライブラリでありながら、URLクエリパラメータ管理において、既存のルーティングライブラリとは一線を画す充実した機能を提供しています。

主な特徴をまとめると:

  1. スキーマベースの型安全性:Zod等のスキーマ定義から自動的に型が生成され、コンパイル時エラー検出が可能
  2. JSON-firstアプローチ:複雑なデータ構造を自然に扱える設計
  3. Search Middleware:組み込みミドルウェアとカスタム実装によるURLの正規化
  4. 細粒度サブスクリプション:必要な部分のみを監視し、不要な再レンダリングを防止
  5. パフォーマンス最適化:カスタムフックによるReact Transitionの活用が可能

これらの機能を活用することで、TanStack RouterはURLクエリパラメータを単なる文字列ではなく、構造化された型安全なデータストアとして扱うことを可能にします。

以上です!

Discussion