Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions docs/router/api/router/retainSearchParamsFunction.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,14 @@ If `true` is passed in, all search params will be retained.
```tsx
import { z } from 'zod'
import { createRootRoute, retainSearchParams } from '@tanstack/react-router'
import { zodValidator } from '@tanstack/zod-adapter'

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

export const Route = createRootRoute({
validateSearch: zodValidator(searchSchema),
// Use the schema directly for Zod v4
validateSearch: searchSchema,
search: {
middlewares: [retainSearchParams(['rootValue'])],
},
Expand All @@ -32,15 +32,15 @@ export const Route = createRootRoute({
```tsx
import { z } from 'zod'
import { createFileRoute, retainSearchParams } from '@tanstack/react-router'
import { zodValidator } from '@tanstack/zod-adapter'

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

export const Route = createFileRoute('/')({
validateSearch: zodValidator(searchSchema),
// Use the schema directly for Zod v4
validateSearch: searchSchema,
search: {
middlewares: [retainSearchParams(true)],
},
Expand Down
12 changes: 6 additions & 6 deletions docs/router/api/router/stripSearchParamsFunction.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ title: Search middleware to strip search params
```tsx
import { z } from 'zod'
import { createFileRoute, stripSearchParams } from '@tanstack/react-router'
import { zodValidator } from '@tanstack/zod-adapter'

const defaultValues = {
one: 'abc',
Expand All @@ -31,7 +30,8 @@ const searchSchema = z.object({
})

export const Route = createFileRoute('/')({
validateSearch: zodValidator(searchSchema),
// for Zod v4, use the schema directly
validateSearch: searchSchema,
search: {
// strip default values
middlewares: [stripSearchParams(defaultValues)],
Expand All @@ -42,15 +42,15 @@ export const Route = createFileRoute('/')({
```tsx
import { z } from 'zod'
import { createRootRoute, stripSearchParams } from '@tanstack/react-router'
import { zodValidator } from '@tanstack/zod-adapter'

const searchSchema = z.object({
hello: z.string().default('world'),
requiredParam: z.string(),
})

export const Route = createRootRoute({
validateSearch: zodValidator(searchSchema),
// for Zod v4, use the schema directly
validateSearch: searchSchema,
search: {
// always remove `hello`
middlewares: [stripSearchParams(['hello'])],
Expand All @@ -61,15 +61,15 @@ export const Route = createRootRoute({
```tsx
import { z } from 'zod'
import { createFileRoute, stripSearchParams } from '@tanstack/react-router'
import { zodValidator } from '@tanstack/zod-adapter'

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

export const Route = createFileRoute('/')({
validateSearch: zodValidator(searchSchema),
// for Zod v4, use the schema directly
validateSearch: searchSchema,
search: {
// remove all search params
middlewares: [stripSearchParams(true)],
Expand Down
33 changes: 27 additions & 6 deletions docs/router/guide/search-params.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,9 @@ For validation libraries we recommend using adapters which infer the correct `in

### Zod

An adapter is provided for [Zod](https://zod.dev/) which will pipe through the correct `input` type and `output` type
An adapter is provided for [Zod](https://zod.dev/) which will pipe through the correct `input` type and `output` type.

For Zod v3:

```tsx
import { zodValidator } from '@tanstack/zod-adapter'
Expand All @@ -213,13 +215,30 @@ export const Route = createFileRoute('/shop/products/')({
})
```

The important part here is the following use of `Link` no longer requires `search` params
With Zod v4, you should directly use the schema in `validateSearch`:

```tsx
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/')({
// With Zod v4, we can use the schema without the adapter
validateSearch: productSearchSchema,
})
```

The important part here is the following use of `Link` no longer requires `search` params:

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

However the use of `catch` here overrides the types and makes `page`, `filter` and `sort` `unknown` causing type loss. We have handled this case by providing a `fallback` generic function which retains the types but provides a `fallback` value when validation fails
In Zod v3, the use of `catch` here overrides the types and makes `page`, `filter` and `sort` `unknown` causing type loss. We have handled this case by providing a `fallback` generic function which retains the types but provides a `fallback` value when validation fails:

```tsx
import { fallback, zodValidator } from '@tanstack/zod-adapter'
Expand All @@ -240,7 +259,9 @@ export const Route = createFileRoute('/shop/products/')({

Therefore when navigating to this route, `search` is optional and retains the correct types.

While not recommended, it is also possible to configure `input` and `output` type in case the `output` type is more accurate than the `input` type
In Zod v4, schemas may use `catch` instead of the fallback and will retain type inference throughout.

While not recommended, it is also possible to configure `input` and `output` type in case the `output` type is more accurate than the `input` type:

```tsx
const productSearchSchema = z.object({
Expand Down Expand Up @@ -457,7 +478,7 @@ Now that you've learned how to read your route's search params, you'll be happy

The best way to update search params is to use the `search` prop on the `<Link />` component.

If the search for the current page shall be updated and the `from` prop is specified, the `to` prop can be omitted.
If the search for the current page shall be updated and the `from` prop is specified, the `to` prop can be omitted.
Here's an example:

```tsx title="src/routes/shop/products.tsx"
Expand All @@ -478,7 +499,7 @@ const ProductList = () => {

If you want to update the search params in a generic component that is rendered on multiple routes, specifying `from` can be challenging.

In this scenario you can set `to="."` which will give you access to loosely typed search params.
In this scenario you can set `to="."` which will give you access to loosely typed search params.
Here is an example that illustrates this:

```tsx
Expand Down
105 changes: 54 additions & 51 deletions docs/router/how-to/setup-basic-search-params.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ Set up search parameters with schema validation (recommended for production):

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

const productSearchSchema = z.object({
Expand All @@ -20,7 +19,7 @@ const productSearchSchema = z.object({
})

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

Expand Down Expand Up @@ -50,7 +49,25 @@ function ProductsPage() {

## Validation Library Setup

TanStack Router supports any standard schema-compliant validation library. This guide focuses on Zod for examples, but you can use any validation library:
TanStack Router supports any standard schema-compliant validation library. This guide focuses on Zod for examples, but you can use any validation library.

Using Zod v4:

```tsx
import { z } from 'zod'

const searchSchema = z.object({
page: z.number().default(1),
category: z.string().default('all').catch('all'),
})

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

For Zod v3:

```bash
npm install zod @tanstack/zod-adapter
Expand All @@ -75,15 +92,9 @@ export const Route = createFileRoute('/products')({

## Step-by-Step Setup with Zod

The rest of this guide uses Zod for examples, but the patterns apply to any validation library.
The rest of this guide uses Zod v4 for examples, but the patterns apply to any validation library.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat -n docs/router/how-to/setup-basic-search-params.md | head -120 | tail -40

Repository: TanStack/router

Length of output: 1472


🏁 Script executed:

cat -n docs/router/how-to/setup-basic-search-params.md | sed -n '120,180p'

Repository: TanStack/router

Length of output: 1789


🏁 Script executed:

cat -n docs/router/how-to/setup-basic-search-params.md | sed -n '180,250p'

Repository: TanStack/router

Length of output: 2176


🏁 Script executed:

cat -n docs/router/how-to/setup-basic-search-params.md | sed -n '250,350p'

Repository: TanStack/router

Length of output: 3740


Remove unused fallback import from Step 1 example.

The Step 1 code imports the Zod v4 adapter but doesn't use it—the example correctly uses native Zod v4 .default() syntax instead. Remove the import to keep the example clean and consistent with the stated v4 approach.

Proposed fix
 import { z } from 'zod'
-import { fallback } from '@tanstack/zod-adapter'
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/router/how-to/setup-basic-search-params.md` at line 95, Remove the
unused `fallback` import from the Step 1 example: locate the import that brings
in the Zod v4 adapter and remove the `fallback` specifier (or the entire adapter
import if it only exported `fallback`), leaving only the native Zod v4 usage
that relies on `.default()`; ensure the example imports exactly the symbols it
uses so there are no unused imports.


### Step 1: Install Dependencies

```bash
npm install zod @tanstack/zod-adapter
```

### Step 2: Define Your Search Schema
### Step 1: Define Your Search Schema

Start by identifying what search parameters your route needs:

Expand All @@ -93,40 +104,38 @@ import { fallback } from '@tanstack/zod-adapter'

const shopSearchSchema = z.object({
// Pagination
page: fallback(z.number(), 1).default(1),
limit: fallback(z.number(), 20).default(20),
page: z.number().default(1),
limit: z.number().default(20),

// Filtering
category: fallback(z.string(), 'all').default('all'),
minPrice: fallback(z.number(), 0).default(0),
maxPrice: fallback(z.number(), 1000).default(1000),
category: z.string().default('all'),
minPrice: z.number().default(0),
maxPrice: z.number().default(1000),

// Settings
sort: fallback(z.enum(['name', 'price', 'date']), 'name').default('name'),
ascending: fallback(z.boolean(), true).default(true),
sort: z.enum(['name', 'price', 'date']).default('name'),
ascending: z.boolean().default(true),

// Optional parameters
searchTerm: z.string().optional(),
showOnlyInStock: fallback(z.boolean(), false).default(false),
showOnlyInStock: z.boolean().default(false),
})

type ShopSearch = z.infer<typeof shopSearchSchema>
```

### Step 3: Add Schema Validation to Route
### Step 2: Add Schema Validation to Route

Use the validation adapter to connect your schema to the route:
Connect your schema to the route:

```tsx
import { zodValidator } from '@tanstack/zod-adapter'

export const Route = createFileRoute('/shop')({
validateSearch: zodValidator(shopSearchSchema),
validateSearch: shopSearchSchema,
component: ShopPage,
})
```

### Step 4: Read Search Parameters in Components
### Step 3: Read Search Parameters in Components

Use the route's `useSearch()` hook to access validated and typed search parameters:

Expand Down Expand Up @@ -166,12 +175,12 @@ function ShopPage() {

```tsx
const paginationSchema = z.object({
page: fallback(z.number().min(1), 1).default(1),
limit: fallback(z.number().min(10).max(100), 20).default(20),
page: z.number().min(1).default(1),
limit: z.number().min(10).max(100).default(20),
})

export const Route = createFileRoute('/posts')({
validateSearch: zodValidator(paginationSchema),
validateSearch: paginationSchema,
component: PostsPage,
})

Expand All @@ -196,16 +205,13 @@ function PostsPage() {

```tsx
const catalogSchema = z.object({
sort: fallback(z.enum(['name', 'date', 'price']), 'name').default('name'),
category: fallback(
z.enum(['electronics', 'clothing', 'books', 'all']),
'all',
).default('all'),
ascending: fallback(z.boolean(), true).default(true),
sort: z.enum(['name', 'date', 'price']).default('name'),
category: z.enum(['electronics', 'clothing', 'books', 'all']).default('all'),
ascending: z.boolean().default(true),
})

export const Route = createFileRoute('/catalog')({
validateSearch: zodValidator(catalogSchema),
validateSearch: catalogSchema,
component: CatalogPage,
})
```
Expand All @@ -215,27 +221,24 @@ export const Route = createFileRoute('/catalog')({
```tsx
const dashboardSchema = z.object({
// Numbers with validation
userId: fallback(z.number().positive(), 1).default(1),
refreshInterval: fallback(z.number().min(1000).max(60000), 5000).default(
5000,
),
userId: z.number().positive().default(1),
refreshInterval: z.number().min(1000).max(60000).default(5000),

// Strings with validation
theme: fallback(z.enum(['light', 'dark']), 'light').default('light'),
theme: z.enum(['light', 'dark']).default('light'),
timezone: z.string().optional(),

// Arrays with validation
selectedIds: fallback(z.number().array(), []).default([]),
tags: fallback(z.string().array(), []).default([]),
selectedIds: z.number().array().default([]),
tags: z.string().array().default([]),

// Objects with validation
filters: fallback(
z.object({
filters: z
.object({
status: z.enum(['active', 'inactive']).optional(),
type: z.string().optional(),
}),
{},
).default({}),
})
.default({}),
})
```

Expand All @@ -245,8 +248,8 @@ const dashboardSchema = z.object({
const reportSchema = z.object({
startDate: z.string().pipe(z.coerce.date()).optional(),
endDate: z.string().pipe(z.coerce.date()).optional(),
format: fallback(z.enum(['pdf', 'csv', 'excel']), 'pdf').default('pdf'),
includeCharts: fallback(z.boolean(), true).default(true),
format: z.enum(['pdf', 'csv', 'excel']).default('pdf').catch('pdf'),
includeCharts: z.boolean().default(true),
})
```

Expand Down Expand Up @@ -330,7 +333,7 @@ export const Route = createFileRoute('/example')({

### Problem: Search Parameters Cause TypeScript Errors

**Cause:** Missing or incorrect schema definition.
**Cause:** Missing or incorrect schema definition with Zod v3.

**Solution:** Ensure your schema covers all search parameters and use proper types:

Expand Down Expand Up @@ -366,7 +369,7 @@ const schema = z.object({

// ✅ Graceful fallback handling
const schema = z.object({
page: fallback(z.number(), 1).default(1), // Safe fallback to 1
page: z.number().default(1).catch(1), // Safe fallback to 1
})
```

Expand Down
Loading