search-params について
URL の検索パラメーターを検証済みの構造化されたアプリ状態として扱う TanStack Router スキルです。型推論、値の読み書き、継承、ミドルウェア、ローダー依存関係を解説します。
主な機能
- 01検索パラメーターをオブジェクトに解析
- 02型推論を伴うパラメーター検証
- 03ルートでのパラメーター読み書き
- 04ミドルウェアによる保持と削除
- 05ローダー依存関係の選択
活用シーン
- 01URL のページ番号や並べ替え値の検証
- 02ページ変更時に既存フィルターを維持
- 03親子ルート間で検索パラメーターを共有
- 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>
参考資料
- 検証パターンのリファレンス — すべての検証ライブラリに対応した包括的なパターン