mcpField
S
search-params

search-params

github.com/tanstack开发15.3k207 次安装4 天前更新
代码仓库

关于 search-params

介绍 TanStack Router 如何将 URL 查询参数作为经过验证的结构化应用状态。内容涵盖类型推断、参数读取与更新、继承、中间件和加载器依赖。

主要功能

  1. 01将查询参数解析为对象
  2. 02验证参数并推断类型
  3. 03在路由中读取和更新参数
  4. 04通过中间件保留或移除参数
  5. 05选择加载器依赖项

适用场景

  1. 01验证 URL 中的分页和排序值
  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 时,请使用 @tanstack/zod-adapter 中的 fallback(),而不是 Zod 的 .catch()。在 Zod 适配器中使用 .catch() 会使输出类型变为 unknown,破坏类型安全。这不适用于 Valibot 或 ArkType(它们使用各自的 fallback 机制)。这也不适用于 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 在 Loader 中使用搜索参数

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:在 zodValidator() 中使用 Zod v3 的 .catch(),而不是适配器的 fallback()
// 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>

参考资料

评论

支持 Markdown