搜索参数

类似于 TanStack Query 如何使处理 React 和 Solid 应用中的服务器状态变得轻而易举,TanStack Router 旨在释放 URL 搜索参数在你的应用中的强大功能。

为什么不直接使用 URLSearchParams 呢?

我们明白,你最近听到了很多“使用平台”的声音,并且在很大程度上,我们同意。但是,我们也认为重要的是要认识到平台在更高级的用例中不足之处,并且我们认为 URLSearchParams 就是其中一种情况。

传统的搜索参数 API 通常会假设以下几件事

  • 搜索参数始终是字符串
  • 它们*大多*是扁平的
  • 使用 URLSearchParams 进行序列化和反序列化已经足够好(剧透警告:并非如此。)
  • 搜索参数的修改与 URL 的路径名紧密耦合,并且必须一起更新,即使路径名没有更改。

然而,现实与这些假设大相径庭。

  • 搜索参数代表应用程序状态,因此不可避免地,我们希望它们具有与其他状态管理器相同的 DX。这意味着能够区分原始值类型,并有效地存储和操作复杂的数据结构,如嵌套数组和对象。
  • 有很多方法可以序列化和反序列化状态,但各有优缺点。你应该能够为你的应用程序选择最好的方法,或者至少获得比 URLSearchParams 更好的默认值。
  • 不变性和结构共享。每次你字符串化和解析 url 搜索参数时,引用完整性和对象标识都会丢失,因为每次新的解析都会创建一个具有唯一内存引用的全新数据结构。如果在其生命周期内未得到妥善管理,这种持续的序列化和解析可能会导致意外和不良的性能问题,尤其是在像 React 这样选择通过不变性跟踪响应式,或者像 Solid 这样通常依赖协调来检测来自反序列化数据源的更改的框架中。
  • 搜索参数虽然是 URL 的重要组成部分,但经常独立于 URL 的路径名而更改。例如,用户可能想要更改分页列表的页码,而无需触及 URL 的路径名。

搜索参数, “OG” 状态管理器

你可能在 URL 中见过像 ?page=3?filter-name=tanner 这样的搜索参数。毫无疑问,这确实是**一种全局状态**,存在于 URL 内部。在 URL 中存储特定状态片段很有价值,因为

  • 用户应该能够
    • Cmd/Ctrl + 单击以在新标签页中打开链接,并可靠地看到他们期望的状态
    • 将你应用程序中的链接添加书签并与他人分享,并确保他们将看到与复制链接时完全相同的状态。
    • 刷新你的应用程序或在页面之间来回导航而不会丢失其状态
  • 开发者应该能够轻松地
    • 在 URL 中添加、删除或修改状态,并具有与其他状态管理器一样出色的 DX
    • 轻松验证来自 URL 的搜索参数,使其格式和类型对于他们的应用程序使用是安全的
    • 读取和写入搜索参数,而无需担心底层的序列化格式

JSON 优先的搜索参数

为了实现上述目标,TanStack Router 内置的第一步是一个强大的搜索参数解析器,它可以自动将 URL 的搜索字符串转换为结构化的 JSON。这意味着你可以在搜索参数中存储任何 JSON 可序列化的数据结构,它将被解析和序列化为 JSON。这比 URLSearchParams 有了巨大的改进,后者对类数组结构和嵌套数据的支持有限。

例如,导航到以下路由

tsx
const link = (
  <Link
    to="/shop"
    search={{
      pageIndex: 3,
      includeCategories: ['electronics', 'gifts'],
      sortBy: 'price',
      desc: true,
    }}
  />
)
const link = (
  <Link
    to="/shop"
    search={{
      pageIndex: 3,
      includeCategories: ['electronics', 'gifts'],
      sortBy: 'price',
      desc: true,
    }}
  />
)

将产生以下 URL

/shop?pageIndex=3&includeCategories=%5B%22electronics%22%2C%22gifts%22%5D&sortBy=price&desc=true
/shop?pageIndex=3&includeCategories=%5B%22electronics%22%2C%22gifts%22%5D&sortBy=price&desc=true

当解析此 URL 时,搜索参数将被准确地转换回以下 JSON

json
{
  "pageIndex": 3,
  "includeCategories": ["electronics", "gifts"],
  "sortBy": "price",
  "desc": true
}
{
  "pageIndex": 3,
  "includeCategories": ["electronics", "gifts"],
  "sortBy": "price",
  "desc": true
}

如果你注意到了,这里发生了一些事情

  • 搜索参数的第一层是扁平的且基于字符串的,就像 URLSearchParams 一样。
  • 第一层中不是字符串的值被准确地保留为实际的数字和布尔值。
  • 嵌套数据结构会自动转换为 URL 安全的 JSON 字符串

🧠 其他工具通常假设搜索参数始终是扁平的且基于字符串的,这就是为什么我们选择在第一层保持与 URLSearchParams 兼容的原因。这最终意味着,即使 TanStack Router 正在将你的嵌套搜索参数作为 JSON 进行管理,其他工具仍然能够写入 URL 并正常读取第一层参数。

验证和类型化搜索参数

尽管 TanStack Router 能够将搜索参数解析为可靠的 JSON,但它们最终仍然来自**面向用户的原始文本输入**。与其他序列化边界类似,这意味着在您使用搜索参数之前,应该将它们验证为你的应用程序可以信任和依赖的格式。

进入验证 + TypeScript!

TanStack Router 提供了方便的 API 用于验证和类型化搜索参数。这一切都始于 RoutevalidateSearch 选项

tsx
// /routes/shop.products.tsx

type ProductSearchSortOptions = 'newest' | 'oldest' | 'price'

type ProductSearch = {
  page: number
  filter: string
  sort: ProductSearchSortOptions
}

export const Route = createFileRoute('/shop/products')({
  validateSearch: (search: Record<string, unknown>): ProductSearch => {
    // validate and parse the search params into a typed state
    return {
      page: Number(search?.page ?? 1),
      filter: (search.filter as string) || '',
      sort: (search.sort as ProductSearchSortOptions) || 'newest',
    }
  },
})
// /routes/shop.products.tsx

type ProductSearchSortOptions = 'newest' | 'oldest' | 'price'

type ProductSearch = {
  page: number
  filter: string
  sort: ProductSearchSortOptions
}

export const Route = createFileRoute('/shop/products')({
  validateSearch: (search: Record<string, unknown>): ProductSearch => {
    // validate and parse the search params into a typed state
    return {
      page: Number(search?.page ?? 1),
      filter: (search.filter as string) || '',
      sort: (search.sort as ProductSearchSortOptions) || 'newest',
    }
  },
})

在上面的示例中,我们正在验证 Route 的搜索参数,并返回一个类型化的 ProductSearch 对象。然后,此类型化对象可用于此路由的其他选项**以及任何子路由!**

验证搜索参数

validateSearch 选项是一个函数,它接收 JSON 解析的(但未验证的)搜索参数作为 Record<string, unknown>,并返回你选择的类型化对象。通常最好为格式错误或意外的搜索参数提供合理的后备方案,以使用户的体验保持不中断。

这是一个例子

tsx
// /routes/shop.products.tsx

type ProductSearchSortOptions = 'newest' | 'oldest' | 'price'

type ProductSearch = {
  page: number
  filter: string
  sort: ProductSearchSortOptions
}

export const Route = createFileRoute('/shop/products')({
  validateSearch: (search: Record<string, unknown>): ProductSearch => {
    // validate and parse the search params into a typed state
    return {
      page: Number(search?.page ?? 1),
      filter: (search.filter as string) || '',
      sort: (search.sort as ProductSearchSortOptions) || 'newest',
    }
  },
})
// /routes/shop.products.tsx

type ProductSearchSortOptions = 'newest' | 'oldest' | 'price'

type ProductSearch = {
  page: number
  filter: string
  sort: ProductSearchSortOptions
}

export const Route = createFileRoute('/shop/products')({
  validateSearch: (search: Record<string, unknown>): ProductSearch => {
    // validate and parse the search params into a typed state
    return {
      page: Number(search?.page ?? 1),
      filter: (search.filter as string) || '',
      sort: (search.sort as ProductSearchSortOptions) || 'newest',
    }
  },
})

这是一个使用 <a href="https://zod.dev/">Zod</a> 库(但你可以随意使用任何你想要的验证库)的示例,用于一步验证和类型化搜索参数

tsx
// /routes/shop.products.tsx

import { z } from 'zod'

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

type ProductSearch = z.infer<typeof productSearchSchema>

export const Route = createFileRoute('/shop/products')({
  validateSearch: (search) => productSearchSchema.parse(search),
})
// /routes/shop.products.tsx

import { z } from 'zod'

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

type ProductSearch = z.infer<typeof productSearchSchema>

export const Route = createFileRoute('/shop/products')({
  validateSearch: (search) => productSearchSchema.parse(search),
})

因为 validateSearch 也接受一个带有 parse 属性的对象,所以可以缩短为

tsx
validateSearch: productSearchSchema
validateSearch: productSearchSchema

在上面的示例中,我们使用了 Zod 的 .catch() 修饰符而不是 .default(),以避免向用户显示错误,因为我们坚信,如果搜索参数格式错误,你可能不希望暂停用户的应用程序体验以显示一个大的错误消息。也就是说,可能有时你**确实想显示错误消息**。在这种情况下,你可以使用 .default() 而不是 .catch()

其工作原理的底层机制依赖于 validateSearch 函数抛出错误。如果抛出错误,则会触发路由的 onError 选项(并且 error.routerCode 将设置为 VALIDATE_SEARCH,并且将渲染 errorComponent 而不是路由的 component,你可以在其中以你喜欢的方式处理搜索参数错误。

适配器

当使用像 <a href="https://zod.dev/">Zod</a> 这样的库来验证搜索参数时,你可能希望在将搜索参数提交到 URL 之前 transform 搜索参数。一个常见的 zod transform 示例是 default

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

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

export const Route = createFileRoute('/shop/products/')({
  validateSearch: productSearchSchema,
})
import { createFileRoute } from '@tanstack/solid-router'
import { z } from 'zod'

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

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

当您尝试导航到此路由时,search 是必需的,这可能会令人惊讶。以下 Link 将类型错误,因为缺少 search

tsx
<Link to="/shop/products" />
<Link to="/shop/products" />

对于验证库,我们建议使用适配器,它可以推断正确的 inputoutput 类型。

Zod

为 <a href="https://zod.dev/">Zod</a> 提供了一个适配器,它将管道传输正确的 input 类型和 output 类型

tsx
import { createFileRoute } from '@tanstack/solid-router'
import { zodValidator } from '@tanstack/zod-adapter'
import { z } from 'zod'

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

export const Route = createFileRoute('/shop/products/')({
  validateSearch: zodValidator(productSearchSchema),
})
import { createFileRoute } from '@tanstack/solid-router'
import { zodValidator } from '@tanstack/zod-adapter'
import { z } from 'zod'

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

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

这里重要的是,以下 Link 的使用不再需要 search 参数

tsx
<Link to="/shop/products" />
<Link to="/shop/products" />

然而,此处 catch 的使用会覆盖类型,并使 pagefiltersort 变为 unknown,从而导致类型丢失。我们通过提供一个 fallback 泛型函数来处理这种情况,该函数保留类型,但在验证失败时提供一个 fallback

tsx
import { createFileRoute } from '@tanstack/solid-router'
import { fallback, zodValidator } from '@tanstack/zod-adapter'
import { z } from 'zod'

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

export const Route = createFileRoute('/shop/products/')({
  validateSearch: zodValidator(productSearchSchema),
})
import { createFileRoute } from '@tanstack/solid-router'
import { fallback, zodValidator } from '@tanstack/zod-adapter'
import { z } from 'zod'

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

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

因此,当导航到此路由时,search 是可选的,并保留正确的类型。

虽然不建议这样做,但也可以配置 inputoutput 类型,以防 output 类型比 input 类型更准确

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

export const Route = createFileRoute('/shop/products/')({
  validateSearch: zodValidator({
    schema: productSearchSchema,
    input: 'output',
    output: 'input',
  }),
})
const productSearchSchema = z.object({
  page: fallback(z.number(), 1).default(1),
  filter: fallback(z.string(), '').default(''),
  sort: fallback(z.enum(['newest', 'oldest', 'price']), 'newest').default(
    'newest',
  ),
})

export const Route = createFileRoute('/shop/products/')({
  validateSearch: zodValidator({
    schema: productSearchSchema,
    input: 'output',
    output: 'input',
  }),
})

这提供了灵活性,让你选择要为导航推断的类型以及要为读取搜索参数推断的类型。

Valibot

警告

Router 期望安装 valibot 1.0 包。

当使用 <a href="https://valibot.dev/">Valibot</a> 时,不需要适配器来确保为导航和读取搜索参数使用正确的 inputoutput 类型。这是因为 valibot 实现了 <a href="https://github.com/standard-schema/standard-schema">Standard Schema</a>

tsx
import { createFileRoute } from '@tanstack/solid-router'
import * as v from 'valibot'

const productSearchSchema = v.object({
  page: v.optional(v.fallback(v.number(), 1), 1),
  filter: v.optional(v.fallback(v.string(), ''), ''),
  sort: v.optional(
    v.fallback(v.picklist(['newest', 'oldest', 'price']), 'newest'),
    'newest',
  ),
})

export const Route = createFileRoute('/shop/products/')({
  validateSearch: productSearchSchema,
})
import { createFileRoute } from '@tanstack/solid-router'
import * as v from 'valibot'

const productSearchSchema = v.object({
  page: v.optional(v.fallback(v.number(), 1), 1),
  filter: v.optional(v.fallback(v.string(), ''), ''),
  sort: v.optional(
    v.fallback(v.picklist(['newest', 'oldest', 'price']), 'newest'),
    'newest',
  ),
})

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

Arktype

警告

Router 期望安装 arktype 2.0-rc 包。

当使用 <a href="https://arktype.io/">ArkType</a> 时,不需要适配器来确保为导航和读取搜索参数使用正确的 inputoutput 类型。这是因为 <a href="https://arktype.io/">ArkType</a> 实现了 <a href="https://github.com/standard-schema/standard-schema">Standard Schema</a>

tsx
import { createFileRoute } from '@tanstack/solid-router'
import { type } from 'arktype'

const productSearchSchema = type({
  page: 'number = 1',
  filter: 'string = ""',
  sort: '"newest" | "oldest" | "price" = "newest"',
})

export const Route = createFileRoute('/shop/products/')({
  validateSearch: productSearchSchema,
})
import { createFileRoute } from '@tanstack/solid-router'
import { type } from 'arktype'

const productSearchSchema = type({
  page: 'number = 1',
  filter: 'string = ""',
  sort: '"newest" | "oldest" | "price" = "newest"',
})

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

Effect/Schema

当使用 <a href="https://effect.website/docs/schema/introduction/">Effect/Schema</a> 时,不需要适配器来确保为导航和读取搜索参数使用正确的 inputoutput 类型。这是因为 <a href="https://effect.website/docs/schema/standard-schema/">Effect/Schema</a> 实现了 <a href="https://github.com/standard-schema/standard-schema">Standard Schema</a>

tsx
import { createFileRoute } from '@tanstack/solid-router'
import { Schema as S } from 'effect'

const productSearchSchema = S.standardSchemaV1(
  S.Struct({
    page: S.NumberFromString.pipe(
      S.optional,
      S.withDefaults({
        constructor: () => 1,
        decoding: () => 1,
      }),
    ),
    filter: S.String.pipe(
      S.optional,
      S.withDefaults({
        constructor: () => '',
        decoding: () => '',
      }),
    ),
    sort: S.Literal('newest', 'oldest', 'price').pipe(
      S.optional,
      S.withDefaults({
        constructor: () => 'newest' as const,
        decoding: () => 'newest' as const,
      }),
    ),
  }),
)

export const Route = createFileRoute('/shop/products/')({
  validateSearch: productSearchSchema,
})
import { createFileRoute } from '@tanstack/solid-router'
import { Schema as S } from 'effect'

const productSearchSchema = S.standardSchemaV1(
  S.Struct({
    page: S.NumberFromString.pipe(
      S.optional,
      S.withDefaults({
        constructor: () => 1,
        decoding: () => 1,
      }),
    ),
    filter: S.String.pipe(
      S.optional,
      S.withDefaults({
        constructor: () => '',
        decoding: () => '',
      }),
    ),
    sort: S.Literal('newest', 'oldest', 'price').pipe(
      S.optional,
      S.withDefaults({
        constructor: () => 'newest' as const,
        decoding: () => 'newest' as const,
      }),
    ),
  }),
)

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

读取搜索参数

一旦你的搜索参数被验证和类型化,你终于可以开始读取和写入它们了。在 TanStack Router 中有几种方法可以做到这一点,让我们来看看。

在 Loaders 中使用搜索参数

请阅读 <a href="/router/latest/docs/framework/solid/guide/data-loading#using-loaderdeps-to-access-search-params">Loaders 中的搜索参数</a> 部分,以获取有关如何在 loaders 中使用 loaderDeps 选项读取搜索参数的更多信息。

搜索参数从父路由继承

当你沿着路由树向下移动时,父级的搜索参数和类型会合并,因此子路由也可以访问其父级的搜索参数

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

type ProductSearch = z.infer<typeof productSearchSchema>

export const Route = createFileRoute('/shop/products')({
  validateSearch: productSearchSchema,
})
const productSearchSchema = z.object({
  page: z.number().catch(1),
  filter: z.string().catch(''),
  sort: z.enum(['newest', 'oldest', 'price']).catch('newest'),
})

type ProductSearch = z.infer<typeof productSearchSchema>

export const Route = createFileRoute('/shop/products')({
  validateSearch: productSearchSchema,
})
  • shop.products.$productId.tsx
tsx
export const Route = createFileRoute('/shop/products/$productId')({
  beforeLoad: ({ search }) => {
    search
    // ^? ProductSearch ✅
  },
})
export const Route = createFileRoute('/shop/products/$productId')({
  beforeLoad: ({ search }) => {
    search
    // ^? ProductSearch ✅
  },
})

在组件中使用搜索参数

你可以通过 useSearch hook在你的路由的 component 中访问路由的已验证搜索参数。

tsx
// /routes/shop.products.tsx

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

const ProductList = () => {
  const { page, filter, sort } = Route.useSearch()

  return <div>...</div>
}
// /routes/shop.products.tsx

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

const ProductList = () => {
  const { page, filter, sort } = Route.useSearch()

  return <div>...</div>
}

提示

如果你的组件是代码分割的,你可以使用 <a href="/router/latest/docs/framework/solid/guide/code-splitting#manually-accessing-route-apis-in-other-files-with-the-getrouteapi-helper">getRouteApi 函数</a> 来避免必须导入 Route 配置以访问类型化的 useSearch() hook。

在路由组件外部使用搜索参数

你可以在应用程序中的任何位置使用 useSearch hook访问路由的已验证搜索参数。通过传递原始路由的 from id/路径,你将获得更好的类型安全性

tsx
// /routes/shop.products.tsx
export const Route = createFileRoute('/shop/products')({
  validateSearch: productSearchSchema,
  // ...
})

// Somewhere else...

// /components/product-list-sidebar.tsx
const routeApi = getRouteApi('/shop/products')

const ProductList = () => {
  const routeSearch = routeApi.useSearch()

  // OR

  const { page, filter, sort } = useSearch({
    from: Route.fullPath,
  })

  return <div>...</div>
}
// /routes/shop.products.tsx
export const Route = createFileRoute('/shop/products')({
  validateSearch: productSearchSchema,
  // ...
})

// Somewhere else...

// /components/product-list-sidebar.tsx
const routeApi = getRouteApi('/shop/products')

const ProductList = () => {
  const routeSearch = routeApi.useSearch()

  // OR

  const { page, filter, sort } = useSearch({
    from: Route.fullPath,
  })

  return <div>...</div>
}

或者,你可以放宽类型安全性,并通过传递 strict: false 来获得一个可选的 search 对象

tsx
function ProductList() {
  const search = useSearch({
    strict: false,
  })
  // {
  //   page: number | undefined
  //   filter: string | undefined
  //   sort: 'newest' | 'oldest' | 'price' | undefined
  // }

  return <div>...</div>
}
function ProductList() {
  const search = useSearch({
    strict: false,
  })
  // {
  //   page: number | undefined
  //   filter: string | undefined
  //   sort: 'newest' | 'oldest' | 'price' | undefined
  // }

  return <div>...</div>
}

写入搜索参数

现在你已经学会了如何读取路由的搜索参数,你将很高兴知道你已经看到了修改和更新它们的主要 API。让我们稍微回顾一下

更新搜索参数的最佳方法是使用 <Link /> 组件上的 search prop。

如果要更新当前页面的搜索并且指定了 from prop,则可以省略 to prop。
这是一个例子

tsx
// /routes/shop.products.tsx
export const Route = createFileRoute('/shop/products')({
  validateSearch: productSearchSchema,
})

const ProductList = () => {
  return (
    <div>
      <Link from={Route.fullPath} search={(prev) => ({ page: prev.page + 1 })}>
        Next Page
      </Link>
    </div>
  )
}
// /routes/shop.products.tsx
export const Route = createFileRoute('/shop/products')({
  validateSearch: productSearchSchema,
})

const ProductList = () => {
  return (
    <div>
      <Link from={Route.fullPath} search={(prev) => ({ page: prev.page + 1 })}>
        Next Page
      </Link>
    </div>
  )
}

如果你想在多个路由上渲染的通用组件中更新搜索参数,则指定 from 可能会具有挑战性。

在这种情况下,你可以设置 to=".",这将使你能够访问松散类型的搜索参数。
这是一个说明这一点的例子

tsx
// `page` is a search param that is defined in the __root route and hence available on all routes.
const PageSelector = () => {
  return (
    <div>
      <Link to="." search={(prev) => ({ ...prev, page: prev.page + 1 })}>
        Next Page
      </Link>
    </div>
  )
}
// `page` is a search param that is defined in the __root route and hence available on all routes.
const PageSelector = () => {
  return (
    <div>
      <Link to="." search={(prev) => ({ ...prev, page: prev.page + 1 })}>
        Next Page
      </Link>
    </div>
  )
}

如果通用组件仅在路由树的特定子树中渲染,则可以使用 from 指定该子树。如果你愿意,可以在这里省略 to='.'

tsx
// `page` is a search param that is defined in the /posts route and hence available on all of its child routes.
const PageSelector = () => {
  return (
    <div>
      <Link
        from="/posts"
        to="."
        search={(prev) => ({ ...prev, page: prev.page + 1 })}
      >
        Next Page
      </Link>
    </div>
  )
// `page` is a search param that is defined in the /posts route and hence available on all of its child routes.
const PageSelector = () => {
  return (
    <div>
      <Link
        from="/posts"
        to="."
        search={(prev) => ({ ...prev, page: prev.page + 1 })}
      >
        Next Page
      </Link>
    </div>
  )

useNavigate(), navigate({ search })

navigate 函数也接受一个 search 选项,其工作方式与 <Link /> 上的 search prop 相同

tsx
// /routes/shop.products.tsx
export const Route = createFileRoute('/shop/products/$productId')({
  validateSearch: productSearchSchema,
})

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

  return (
    <div>
      <button
        onClick={() => {
          navigate({
            search: (prev) => ({ page: prev.page + 1 }),
          })
        }}
      >
        Next Page
      </button>
    </div>
  )
}
// /routes/shop.products.tsx
export const Route = createFileRoute('/shop/products/$productId')({
  validateSearch: productSearchSchema,
})

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

  return (
    <div>
      <button
        onClick={() => {
          navigate({
            search: (prev) => ({ page: prev.page + 1 }),
          })
        }}
      >
        Next Page
      </button>
    </div>
  )
}

router.navigate({ search })

router.navigate 函数的工作方式与上面的 useNavigate/navigate hook/函数完全相同。

<Navigate search /> 组件的工作方式与上面的 useNavigate/navigate hook/函数完全相同,但接受其选项作为 props 而不是函数参数。

使用搜索中间件转换 search

当构建链接 href 时,默认情况下,对于查询字符串部分,唯一重要的是 <Link>search 属性。

TanStack Router 提供了一种在生成 href 之前通过 **搜索中间件** 来操作搜索参数的方法。搜索中间件是在为路由或其后代生成新链接时转换搜索参数的函数。它们也在搜索验证后的导航时执行,以允许操作查询字符串。

以下示例显示如何确保对于正在构建的**每个**链接,都添加 rootValue 搜索参数(*如果*它是当前搜索参数的一部分)。如果链接在 search 内部指定了 rootValue,则该值用于构建链接。

tsx
import { z } from 'zod'
import { createFileRoute } from '@tanstack/solid-router'
import { zodValidator } from '@tanstack/zod-adapter'

const searchSchema = z.object({
  rootValue: z.string().optional(),
})

export const Route = createRootRoute({
  validateSearch: zodValidator(searchSchema),
  search: {
    middlewares: [
      ({search, next}) => {
        const result = next(search)
        return {
          rootValue: search.rootValue
          ...result
        }
      }
    ]
  }
})
import { z } from 'zod'
import { createFileRoute } from '@tanstack/solid-router'
import { zodValidator } from '@tanstack/zod-adapter'

const searchSchema = z.object({
  rootValue: z.string().optional(),
})

export const Route = createRootRoute({
  validateSearch: zodValidator(searchSchema),
  search: {
    middlewares: [
      ({search, next}) => {
        const result = next(search)
        return {
          rootValue: search.rootValue
          ...result
        }
      }
    ]
  }
})

由于这种特定用例非常常见,TanStack Router 提供了一个通用实现,通过 retainSearchParams 保留搜索参数

tsx
import { z } from 'zod'
import { createFileRoute, retainSearchParams } from '@tanstack/solid-router'
import { zodValidator } from '@tanstack/zod-adapter'

const searchSchema = z.object({
  rootValue: z.string().optional(),
})

export const Route = createRootRoute({
  validateSearch: zodValidator(searchSchema),
  search: {
    middlewares: [retainSearchParams(['rootValue'])],
  },
})
import { z } from 'zod'
import { createFileRoute, retainSearchParams } from '@tanstack/solid-router'
import { zodValidator } from '@tanstack/zod-adapter'

const searchSchema = z.object({
  rootValue: z.string().optional(),
})

export const Route = createRootRoute({
  validateSearch: zodValidator(searchSchema),
  search: {
    middlewares: [retainSearchParams(['rootValue'])],
  },
})

另一个常见的用例是从链接中剥离搜索参数(如果已设置其默认值)。TanStack Router 通过 stripSearchParams 为此用例提供了通用实现

tsx
import { z } from 'zod'
import { createFileRoute, stripSearchParams } from '@tanstack/solid-router'
import { zodValidator } from '@tanstack/zod-adapter'

const defaultValues = {
  one: 'abc',
  two: 'xyz',
}

const searchSchema = z.object({
  one: z.string().default(defaultValues.one),
  two: z.string().default(defaultValues.two),
})

export const Route = createFileRoute('/hello')({
  validateSearch: zodValidator(searchSchema),
  search: {
    // strip default values
    middlewares: [stripSearchParams(defaultValues)],
  },
})
import { z } from 'zod'
import { createFileRoute, stripSearchParams } from '@tanstack/solid-router'
import { zodValidator } from '@tanstack/zod-adapter'

const defaultValues = {
  one: 'abc',
  two: 'xyz',
}

const searchSchema = z.object({
  one: z.string().default(defaultValues.one),
  two: z.string().default(defaultValues.two),
})

export const Route = createFileRoute('/hello')({
  validateSearch: zodValidator(searchSchema),
  search: {
    // strip default values
    middlewares: [stripSearchParams(defaultValues)],
  },
})

可以链式调用多个中间件。以下示例显示了如何组合 retainSearchParamsstripSearchParams

tsx
import {
  Link,
  createFileRoute,
  retainSearchParams,
  stripSearchParams,
} from '@tanstack/solid-router'
import { z } from 'zod'
import { zodValidator } from '@tanstack/zod-adapter'

const defaultValues = ['foo', 'bar']

export const Route = createFileRoute('/search')({
  validateSearch: zodValidator(
    z.object({
      retainMe: z.string().optional(),
      arrayWithDefaults: z.string().array().default(defaultValues),
      required: z.string(),
    }),
  ),
  search: {
    middlewares: [
      retainSearchParams(['retainMe']),
      stripSearchParams({ arrayWithDefaults: defaultValues }),
    ],
  },
})
import {
  Link,
  createFileRoute,
  retainSearchParams,
  stripSearchParams,
} from '@tanstack/solid-router'
import { z } from 'zod'
import { zodValidator } from '@tanstack/zod-adapter'

const defaultValues = ['foo', 'bar']

export const Route = createFileRoute('/search')({
  validateSearch: zodValidator(
    z.object({
      retainMe: z.string().optional(),
      arrayWithDefaults: z.string().array().default(defaultValues),
      required: z.string(),
    }),
  ),
  search: {
    middlewares: [
      retainSearchParams(['retainMe']),
      stripSearchParams({ arrayWithDefaults: defaultValues }),
    ],
  },
})
订阅 Bytes

您每周的 JavaScript 新闻。每周一免费发送给超过 100,000 名开发者。

Bytes

无垃圾邮件。随时取消订阅。