Skip to content
路由参数与搜索参数

路由参数与搜索参数

TanStack Router 将 URL 参数分为两类并提供完整的类型安全:路径参数:param)和搜索参数?key=value)。搜索参数是 TanStack Router 的一大亮点——它们被视为"JSON 序列化的应用状态",而非简单的字符串。


路径参数(Path Params)

定义

使用 $ 前缀在文件名中定义动态路径段:

// src/routes/users/$userId.tsx
import { createFileRoute } from '@tanstack/react-router'

export const Route = createFileRoute('/users/$userId')({
  component: UserProfile,
})

读取路径参数

function UserProfile() {
  // 类型自动推断:{ userId: string }
  const { userId } = Route.useParams()
  return <div>用户 ID: {userId}</div>
}

// 或者从其他组件访问
import { useParams } from '@tanstack/react-router'
function UserAvatar() {
  const { userId } = useParams({ from: '/users/$userId' })
}

参数解析与序列化

默认情况下路径参数都是 string。你可以通过 parse/stringify 进行类型转换:

export const Route = createFileRoute('/users/$userId')({
  params: {
    parse: (params) => ({
      userId: Number(params.userId),  // string → number
    }),
    stringify: ({ userId }) => ({
      userId: String(userId),          // number → string
    }),
  },
  component: UserProfile,
})

function UserProfile() {
  const { userId } = Route.useParams()
  // userId 现在类型为 number ✅
}

💡 最佳实践parsestringify 必须是双向可逆的——parse(stringify(x)) 应等于 x。如果 parse 失败(如 NaN),路由会跳转到 notFoundComponent


搜索参数(Search Params)

搜索参数在 TanStack Router 中是一等公民——它们被自动 JSON 解析,支持类型验证,并且可以从父路由继承。

基础验证

// src/routes/posts/index.tsx
export const Route = createFileRoute('/posts/')({
  validateSearch: (search: Record<string, unknown>) => {
    return {
      page: Number(search.page) || 1,
      filter: String(search.filter || ''),
      sort: (search.sort as 'newest' | 'oldest') || 'newest',
    }
  },
  component: PostsPage,
})

function PostsPage() {
  // 类型完全推断,无需手动标注
  const { page, filter, sort } = Route.useSearch()
  // page: number
  // filter: string
  // sort: 'newest' | 'oldest'
}

Zod 验证(推荐)

使用 @tanstack/zod-adapter 获得更强大的验证:

npm install zod @tanstack/zod-adapter
import { z } from 'zod'
import { fallback, zodValidator } from '@tanstack/zod-adapter'

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

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

🚨 陷阱(Zod v3):务必使用 fallback() 而非 .catch()。Zod v3 的 .catch() 会使输出类型变为 unknown,破坏类型推断。Zod v4 已修复此问题。

读取搜索参数

function ProductsPage() {
  const { page, filter, sort } = Route.useSearch()
  // 全部类型安全,带默认值

  return (
    <div>
      <span>当前页: {page}</span>
      <span>排序: {sort}</span>
    </div>
  )
}

多种访问方式

// 方式一:Route.useSearch()(最推荐,最类型安全)
const search = Route.useSearch()

// 方式二:getRouteApi(跨组件访问)
import { getRouteApi } from '@tanstack/react-router'
const routeApi = getRouteApi('/products')
const search = routeApi.useSearch()

// 方式三:通用 Hook
import { useSearch } from '@tanstack/react-router'
const search = useSearch({ from: '/products' })

// 方式四:宽松模式(跨路由共享搜索参数)
const search = useSearch({ strict: false })
// 返回值类型是宽松的,key 对应的类型是各路由的联合

搜索参数的继承

子路由自动继承所有祖先路由的搜索参数:

// __root.tsx — 定义全局搜索参数
export const Route = createRootRoute({
  validateSearch: z.object({
    theme: z.enum(['light', 'dark']).default('light'),
    utm_source: z.string().optional(),
  }),
})

// posts/index.tsx — 自动继承 theme 和 utm_source
export const Route = createFileRoute('/posts/')({
  validateSearch: z.object({
    page: fallback(z.number(), 1).default(1),
    // theme 自动可用!无需重复定义
  }),
})

function PostsPage() {
  const search = Route.useSearch()
  // search: { page: number; theme: 'light' | 'dark'; utm_source?: string }
  search.theme // ✅ 可用
}

🔬 深入原理:搜索参数的继承是在构建时由 Vite 插件计算的。路由树生成时会合并祖先链上所有的 validateSearch 定义。如果祖先路由没有定义 validateSearch,子路由就无法继承任何参数。


搜索参数中间件(Middlewares)

retainSearchParams — 跨导航保留参数

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

export const Route = createRootRoute({
  validateSearch: z.object({
    debug: z.boolean().optional(),
    theme: z.enum(['light', 'dark']).default('light'),
  }),
  search: {
    middlewares: [retainSearchParams(['debug', 'theme'])],
  },
})
// debug 和 theme 参数在所有导航中都会被保留

stripSearchParams — 移除默认值

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

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

export const Route = createFileRoute('/items')({
  validateSearch: searchSchema,
  search: {
    middlewares: [stripSearchParams(defaults)],
  },
})
// 当 sort='newest' 且 page=1 时,这些参数不会出现在 URL 中

loaderDeps — 精确控制 Loader 触发

默认情况下,任何搜索参数变化都会重新执行 loader。用 loaderDeps 精确控制:

export const Route = createFileRoute('/posts/')({
  validateSearch: postSearchSchema,
  // 只有当 page 变化时才重新执行 loader
  // sort、filter 等变化不会触发 loader
  loaderDeps: ({ search }) => ({
    page: search.page,
  }),
  loader: async ({ deps }) => {
    return fetchPosts({ page: deps.page })
  },
})

性能提示:始终使用 loaderDeps 选择 loader 真正依赖的字段。传入整个 search 对象会导致每次搜索参数变化都重新请求,浪费资源。


常见模式

列表分页(配合函数式更新)

function Pagination() {
  const navigate = useNavigate({ from: '/posts/' })
  const { page } = Route.useSearch()

  return (
    <div>
      <button
        onClick={() =>
          navigate({ search: (prev) => ({ ...prev, page: page - 1 }) })
        }
        disabled={page <= 1}
      >
        上一页
      </button>
      <span> {page} </span>
      <button
        onClick={() =>
          navigate({ search: (prev) => ({ ...prev, page: page + 1 }) })
        }
      >
        下一页
      </button>
    </div>
  )
}

排序切换

function SortSelect() {
  const navigate = useNavigate({ from: '/products' })
  const { sort, page } = Route.useSearch()

  return (
    <select
      value={sort}
      onChange={(e) =>
        navigate({
          search: (prev) => ({
            ...prev,
            sort: e.target.value as 'newest' | 'oldest',
            page: 1, // 排序变化时重置到第一页
          }),
        })
      }
    >
      <option value="newest">最新</option>
      <option value="oldest">最旧</option>
    </select>
  )
}

搜索过滤

function SearchFilter() {
  const navigate = useNavigate({ from: '/products' })
  const { filter, page } = Route.useSearch()

  return (
    <input
      value={filter}
      placeholder="搜索..."
      onChange={(e) => {
        // 防抖处理在实际项目中很重要
        navigate({
          search: (prev) => ({
            ...prev,
            filter: e.target.value,
            page: 1, // 过滤条件变化时重置到第一页
          }),
          replace: true, // 避免每个按键都产生历史记录
        })
      }}
    />
  )
}

路径参数 vs 搜索参数 选择指南

维度 路径参数 ($param) 搜索参数 (?key=value)
语义 标识"哪个资源" 描述"如何展示"
必填性 必填(路径结构的一部分) 可选(可设默认值)
例子 /users/42, /posts/hello-world ?page=2, ?sort=newest
SEO 对搜索引擎更友好 通常被忽略
类型 通过 parse/stringify 转换 通过 validateSearch 验证
继承 不继承 自动从祖先路由继承

💡 最佳实践:用路径参数标识资源 ID(用户、文章、产品),用搜索参数控制展示方式(分页、排序、过滤、主题)。