mcpField
S
search-params

search-params

github.com/tanstack開発15.3k207 回インストール4 日前に更新
リポジトリ

search-params について

URL の検索パラメーターを検証済みの構造化されたアプリ状態として扱う TanStack Router スキルです。型推論、値の読み書き、継承、ミドルウェア、ローダー依存関係を解説します。

主な機能

  1. 01検索パラメーターをオブジェクトに解析
  2. 02型推論を伴うパラメーター検証
  3. 03ルートでのパラメーター読み書き
  4. 04ミドルウェアによる保持と削除
  5. 05ローダー依存関係の選択

活用シーン

  1. 01URL のページ番号や並べ替え値の検証
  2. 02ページ変更時に既存フィルターを維持
  3. 03親子ルート間で検索パラメーターを共有
  4. 04関連する値の変更時だけローダーを再実行

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/tanstack/router --skill search-params

インストール後、Claude Code で/search-paramsを入力して読み込まれたか確認します。

同梱ファイルreferences/validation-patterns.md

詳細

検索パラメーター

TanStack Router では、検索パラメーターを JSON ファーストのアプリケーション状態として扱います。URL から構造化オブジェクト(数値、ブール値、配列、ネストされたオブジェクト)に自動的にパースされ、各ルートで validateSearch を使って検証されます。

CRITICAL:zodValidator() と Zod v3 を使用する場合は、Zod の .catch() ではなく、@tanstack/zod-adapter の fallback() を使用してください。Zod アダプターで .catch() を使用すると、出力型が unknown になり、型安全性が失われます。これは、独自のフォールバック機構を使用する Valibot または ArkType には該当しません。また、Zod v4 にも該当しません。Zod v4 では .catch() を使用し、zodValidator() は使用しないでください。 CRITICAL:型は完全に推論されます。useSearch() の戻り値に型注釈を付けないでください。

セットアップ:Zod アダプター(推奨)

npm install zod @tanstack/zod-adapter
// src/routes/products.tsx
import { createFileRoute } from '@tanstack/react-router'
import { z } from 'zod'

const productSearchSchema = z.object({
  page: z.number().default(1).catch(1),
  filter: z.string().default(''),
  sort: z.enum(['newest', 'oldest', 'price']).default('newest').catch('newest'),
})

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

function ProductsPage() {
  // page: number, filter: string, sort: 'newest' | 'oldest' | 'price'
  // ALL INFERRED — do not annotate
  const { page, filter, sort } = Route.useSearch()

  return (
    <div>
      <p>
        Page {page}, filter: {filter}, sort: {sort}
      </p>
    </div>
  )
}

検索パラメーターの読み取り

ルートコンポーネント内:Route.useSearch()
function ProductsPage() {
  const { page, sort } = Route.useSearch()
  return <div>Page {page}</div>
}
コード分割されたコンポーネント内:getRouteApi()
import { getRouteApi } from '@tanstack/react-router'

const routeApi = getRouteApi('/products')

function ProductFilters() {
  const { sort } = routeApi.useSearch()
  return <select value={sort}>{/* options */}</select>
}
任意のコンポーネントから:useSearch({ from })
import { useSearch } from '@tanstack/react-router'

function SortIndicator() {
  const { sort } = useSearch({ from: '/products' })
  return <span>Sorted by: {sort}</span>
}
緩やかなアクセス:useSearch({ strict: false })
function GenericPaginator() {
  const search = useSearch({ strict: false })
  // search.page is number | undefined (union of all routes)
  return <span>Page: {search.page ?? 1}</span>
}

検索パラメーターの書き込み

関数形式の Link(既存のパラメーターを保持)
import { Link } from '@tanstack/react-router'

function Pagination() {
  return (
    <Link
      from="/products"
      search={(prev) => ({ ...prev, page: prev.page + 1 })}
    >
      Next Page
    </Link>
  )
}
オブジェクト形式の Link(すべてのパラメーターを置き換え)
<Link to="/products" search={{ page: 1, filter: '', sort: 'newest' }}>
  Reset
</Link>
プログラムからの操作:useNavigate()
import { useNavigate } from '@tanstack/react-router'

function SortDropdown() {
  const navigate = useNavigate({ from: '/products' })

  return (
    <select
      onChange={(e) => {
        navigate({
          search: (prev) => ({ ...prev, sort: e.target.value, page: 1 }),
        })
      }}
    >
      <option value="newest">Newest</option>
      <option value="price">Price</option>
    </select>
  )
}

検索パラメーターの継承

親ルートの検索パラメーターは、子ルートに自動的にマージされます。

// src/routes/shop.tsx — parent defines shared params
import { createFileRoute } from '@tanstack/react-router'
import { z } from 'zod'

const shopSearchSchema = z.object({
  currency: z.enum(['USD', 'EUR']).default('USD').catch('USD'),
})

export const Route = createFileRoute('/shop')({
  validateSearch: shopSearchSchema,
})
// src/routes/shop/products.tsx — child inherits currency
import { createFileRoute } from '@tanstack/react-router'

export const Route = createFileRoute('/shop/products')({
  component: ShopProducts,
})

function ShopProducts() {
  // currency is available here from parent — fully typed
  const { currency } = Route.useSearch()
  return <div>Currency: {currency}</div>
}

検索ミドルウェア

retainSearchParams — ナビゲーション間でパラメーターを保持
import { createRootRoute, retainSearchParams } from '@tanstack/react-router'
import { z } from 'zod'

const rootSearchSchema = z.object({
  debug: z.boolean().optional(),
})

export const Route = createRootRoute({
  validateSearch: rootSearchSchema,
  search: {
    middlewares: [retainSearchParams(['debug'])],
  },
})
stripSearchParams — URL からデフォルト値を削除
import { createFileRoute, stripSearchParams } from '@tanstack/react-router'
import { z } from 'zod'

const defaults = { sort: 'newest', page: 1 }

const searchSchema = z.object({
  sort: z.string().default(defaults.sort),
  page: z.number().default(defaults.page),
})

export const Route = createFileRoute('/items')({
  validateSearch: searchSchema,
  search: {
    middlewares: [stripSearchParams(defaults)],
  },
})
ミドルウェアの連結
export const Route = createFileRoute('/search')({
  validateSearch: z.object({
    retainMe: z.string().optional(),
    arrayWithDefaults: z.string().array().default(['foo', 'bar']),
    required: z.string(),
  }),
  search: {
    middlewares: [
      retainSearchParams(['retainMe']),
      stripSearchParams({ arrayWithDefaults: ['foo', 'bar'] }),
    ],
  },
})

カスタムシリアライズ

ルーターのレベルでデフォルトの JSON シリアライズをオーバーライドします。

import {
  createRouter,
  parseSearchWith,
  stringifySearchWith,
} from '@tanstack/react-router'

const router = createRouter({
  routeTree,
  // Example: use JSURL2 for compact, human-readable URLs
  parseSearch: parseSearchWith(parse),
  stringifySearch: stringifySearchWith(stringify),
})

loaderDeps を使ってローダーで検索パラメーターを使用する

export const Route = createFileRoute('/products')({
  validateSearch: productSearchSchema,
  // Pick ONLY the params the loader needs — not the entire search object
  loaderDeps: ({ search }) => ({ page: search.page }),
  loader: async ({ deps }) => {
    return fetchProducts({ page: deps.page })
  },
})

よくある間違い

1. HIGH:アダプターの fallback() ではなく、zodValidator() で Zod v3 の .catch() を使用する
// WRONG — .catch() with zodValidator makes the type unknown
const schema = z.object({ page: z.number().catch(1) })
validateSearch: zodValidator(schema) // page is typed as unknown!

// CORRECT — fallback() preserves the inferred type
import { fallback } from '@tanstack/zod-adapter'
const schema = z.object({ page: fallback(z.number(), 1) })

重要: これは Zod v3 を使用する場合にのみ該当し、Zod v4 には該当しません。v4 では .catch() を使用するのが正しい方法です。

2. HIGH:loaderDeps から検索オブジェクト全体を返す
// WRONG — loader re-runs on ANY search param change
loaderDeps: ({ search }) => search

// CORRECT — loader only re-runs when page changes
loaderDeps: ({ search }) => ({ page: search.page })
3. HIGH:検索パラメーターに Date オブジェクトを渡す
// WRONG — Date does not serialize correctly to JSON in URLs
<Link search={{ startDate: new Date() }}>

// CORRECT — convert to ISO string
<Link search={{ startDate: new Date().toISOString() }}>
4. MEDIUM:親ルートに validateSearch がないと継承が妨げられる
// WRONG — child cannot access shared params
export const Route = createRootRoute({
  component: RootComponent,
  // no validateSearch!
})

// CORRECT — parent must define validateSearch for children to inherit
export const Route = createRootRoute({
  validateSearch: globalSearchSchema,
  component: RootComponent,
})
5. HIGH(スキル横断):検索パラメーターにオブジェクトではなく関数を使うと、パラメーターが失われる
// WRONG — replaces ALL search params, losing any existing ones
<Link to="." search={{ page: 2 }}>Page 2</Link>

// CORRECT — preserves existing params, updates only page
<Link to="." search={(prev) => ({ ...prev, page: 2 })}>Page 2</Link>

参考資料

コメント