Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
215 changes: 215 additions & 0 deletions docs/router/framework/react/api/router/persistSearchParamsFunction.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
---
id: persistSearchParams
title: Search middleware to persist search params
---

`persistSearchParams` is a search middleware that automatically saves and restores search parameters when navigating between routes.

## persistSearchParams props

`persistSearchParams` accepts one of the following inputs:

- `undefined` (no arguments): persist all search params
- a list of keys of those search params that shall be excluded from persistence

## How it works

The middleware has two main functions:

1. **Saving**: Automatically saves search parameters when they change
2. **Restoring**: Restores saved parameters when the middleware is triggered with empty search

Comment on lines +20 to +21
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

Grammar nit: add article for clarity

“with empty search” → “with an empty search”.

Apply this diff:

-2. **Restoring**: Restores saved parameters when the middleware is triggered with empty search
+2. **Restoring**: Restores saved parameters when the middleware is triggered with an empty search
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
2. **Restoring**: Restores saved parameters when the middleware is triggered with empty search
2. **Restoring**: Restores saved parameters when the middleware is triggered with an empty search
🤖 Prompt for AI Agents
In docs/router/framework/react/api/router/persistSearchParamsFunction.md around
lines 20 to 21, change the phrase "with empty search" to "with an empty search"
to add the missing article for grammatical clarity; update the sentence
accordingly so it reads "Restores saved parameters when the middleware is
triggered with an empty search."

**Important**: The middleware only runs when search parameters are being processed. This means:

- **Without search prop**: `<Link to="/users">` → Middleware doesn't run → No restoration
- **With search function**: `<Link to="/users" search={(prev) => prev}>` → Middleware runs → Restoration happens
- **With explicit search**: `<Link to="/users" search={{ name: 'John' }}>` → Middleware runs → No restoration (params provided)

## Restoration Behavior

⚠️ **Unexpected behavior warning**: If you use the persistence middleware but navigate without the `search` prop, the middleware will only trigger later when you modify search parameters. This can cause unexpected restoration of saved parameters mixed with your new changes.

**Recommended**: Always be explicit about restoration intent using the `search` prop.

Comment on lines +28 to +33
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Document “from” vs “to” route middleware invocation to avoid confusion/leakage.

Given the known behavior that search middlewares run for both the originating (“from”) and destination (“to”) routes, the docs should call this out to explain why an explicit allow-list is required. This directly addresses the bug noted in the PR conversation.

 ## Restoration Behavior
@@
-**Recommended**: Always be explicit about restoration intent using the `search` prop.
+**Recommended**: Always be explicit about restoration intent using the `search` prop.
+
+Note: Search middlewares run for both the originating (“from”) and destination (“to”) routes involved in a navigation. To prevent unintended cross-route persistence, `persistSearchParams` requires an explicit allow‑list (`persistedSearchParams`) and supports an optional `exclude` list. This ensures only the intended keys are saved/restored for the target route.

If helpful, I can add a minimal diagram showing “from” and “to” execution points.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
## Restoration Behavior
⚠️ **Unexpected behavior warning**: If you use the persistence middleware but navigate without the `search` prop, the middleware will only trigger later when you modify search parameters. This can cause unexpected restoration of saved parameters mixed with your new changes.
**Recommended**: Always be explicit about restoration intent using the `search` prop.
## Restoration Behavior
⚠️ **Unexpected behavior warning**: If you use the persistence middleware but navigate without the `search` prop, the middleware will only trigger later when you modify search parameters. This can cause unexpected restoration of saved parameters mixed with your new changes.
**Recommended**: Always be explicit about restoration intent using the `search` prop.
Note: Search middlewares run for both the originating (“from”) and destination (“to”) routes involved in a navigation. To prevent unintended cross-route persistence, `persistSearchParams` requires an explicit allow-list (`persistedSearchParams`) and supports an optional `exclude` list. This ensures only the intended keys are saved/restored for the target route.
🤖 Prompt for AI Agents
In docs/router/framework/react/api/router/persistSearchParamsFunction.md around
lines 28 to 33, the docs warn about unexpected restoration but don't explain
that search middleware is invoked for both the originating ("from") and
destination ("to") routes; update this section to explicitly state that
middleware runs at both "from" and "to" execution points, show how that can
cause parameter leakage, and clarify that this is why an explicit allow-list (or
explicit use of the search prop) is required; add a brief example or minimal
diagram showing the "from" and "to" invocation points to illustrate the flow.

## Examples

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

const usersSearchSchema = z.object({
name: z.string().optional().catch(''),
status: z.enum(['active', 'inactive', 'all']).optional().catch('all'),
page: z.number().optional().catch(0),
})

export const Route = createFileRoute('/users')({
validateSearch: usersSearchSchema,
search: {
// persist all search params
middlewares: [persistSearchParams()],
},
})
```

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

const productsSearchSchema = z.object({
category: z.string().optional(),
minPrice: z.number().optional(),
maxPrice: z.number().optional(),
tempFilter: z.string().optional(),
})

export const Route = createFileRoute('/products')({
validateSearch: productsSearchSchema,
search: {
// exclude tempFilter from persistence
middlewares: [persistSearchParams(['tempFilter'])],
},
})
```

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

const searchSchema = z.object({
category: z.string().optional(),
sortBy: z.string().optional(),
sortOrder: z.string().optional(),
tempFilter: z.string().optional(),
})

export const Route = createFileRoute('/products')({
validateSearch: searchSchema,
search: {
// exclude tempFilter and sortBy from persistence
middlewares: [persistSearchParams(['tempFilter', 'sortBy'])],
},
})
```

## Restoration Patterns

### Automatic Restoration with Links

Use `search={(prev) => prev}` to trigger middleware restoration:

```tsx
import { Link } from '@tanstack/react-router'

function Navigation() {
return (
<div>
{/* Full restoration - restores all saved parameters */}
<Link to="/users" search={(prev) => prev}>
Users
</Link>

{/* Partial override - restore saved params but override specific ones */}
<Link
to="/products"
search={(prev) => ({ ...prev, category: 'Electronics' })}
>
Electronics Products
</Link>

{/* Clean navigation - no restoration */}
<Link to="/users">Users (clean slate)</Link>
</div>
)
}
```

### Exclusion Strategies

You have two ways to exclude parameters from persistence:

**1. Middleware-level exclusion** (permanent):

```tsx
// These parameters are never saved
middlewares: [persistSearchParams(['tempFilter', 'sortBy'])]
```

**2. Link-level exclusion** (per navigation):

```tsx
// Restore saved params but exclude specific ones
<Link
to="/products"
search={(prev) => {
const { tempFilter, ...rest } = prev || {}
return rest
}}
>
Products (excluding temp filter)
</Link>
```

### Manual Restoration

Access the store directly for full control:

```tsx
import { getSearchPersistenceStore, Link } from '@tanstack/react-router'

function CustomNavigation() {
const store = getSearchPersistenceStore()
const savedUsersSearch = store.getSearch('/users')

return (
<Link to="/users" search={savedUsersSearch || {}}>
Users (with saved search)
</Link>
)
}
```

## Using the search persistence store

You can also access the search persistence store directly for manual control:

```tsx
import { getSearchPersistenceStore } from '@tanstack/react-router'

// Get the fully typed store instance
const store = getSearchPersistenceStore()

// Get persisted search for a route
const savedSearch = store.getSearch('/users')

// Clear persisted search for a specific route
store.clearSearch('/users')

// Clear all persisted searches
store.clearAllSearches()

// Manually save search for a route
store.saveSearch('/users', { name: 'John', status: 'active' })
```

```tsx
import { getSearchPersistenceStore } from '@tanstack/react-router'
import { useStore } from '@tanstack/react-store'
import React from 'react'

function MyComponent() {
const store = getSearchPersistenceStore()
const storeState = useStore(store.store)

const clearUserSearch = () => {
store.clearSearch('/users')
}

return (
<div>
<p>Saved search: {JSON.stringify(storeState['/users'])}</p>
<button onClick={clearUserSearch}>Clear saved search</button>
</div>
)
}
```
97 changes: 97 additions & 0 deletions examples/react/search-persistence/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# Search Persistence Example

This example demonstrates TanStack Router's search persistence middleware, which automatically saves and restores search parameters when navigating between routes.

## Overview

The `persistSearchParams` middleware provides seamless search parameter persistence across route navigation. Search parameters are automatically saved when you leave a route and restored when you return, maintaining user context and improving UX.

## Key Features

- **Automatic Persistence**: Search parameters are saved/restored automatically
- **Selective Exclusion**: Choose which parameters to exclude from persistence
- **Type Safety**: Full TypeScript support with automatic type inference
- **Manual Control**: Direct store access for advanced use cases

## Basic Usage

```tsx
import { createFileRoute, persistSearchParams } from '@tanstack/react-router'

// Persist all search parameters
export const Route = createFileRoute('/users')({
validateSearch: usersSearchSchema,
search: {
middlewares: [persistSearchParams()],
},
})

// Exclude specific parameters from persistence
export const Route = createFileRoute('/products')({
validateSearch: productsSearchSchema,
search: {
middlewares: [persistSearchParams(['tempFilter', 'sortBy'])],
},
})
```

## Restoration Patterns

⚠️ **Important**: The middleware only runs when search parameters are being processed. Always be explicit about your restoration intent.

### Automatic Restoration

```tsx
import { Link } from '@tanstack/react-router'

// Full restoration - restores all saved parameters
<Link to="/users" search={(prev) => prev}>
Users (restore all)
</Link>

// Partial override - restore but override specific parameters
<Link to="/products" search={(prev) => ({ ...prev, category: 'Electronics' })}>
Electronics Products
</Link>

// Clean navigation - no restoration
<Link to="/users">
Users (clean slate)
</Link>
```

### Manual Restoration

Access the store directly for full control:

```tsx
import { getSearchPersistenceStore } from '@tanstack/react-router'

const store = getSearchPersistenceStore()
const savedSearch = store.getSearch('/users')

<Link to="/users" search={savedSearch || {}}>
Users (manual restoration)
</Link>
```

### ⚠️ Unexpected Behavior Warning

If you use the persistence middleware but navigate without the `search` prop, restoration will only trigger later when you modify search parameters. This can cause saved parameters to unexpectedly appear mixed with your new changes.

**Recommended**: Always use the `search` prop to be explicit about restoration intent.

## Try It

1. Navigate to `/users` and search for a name
2. Navigate to `/products` and set some filters
3. Use the test links on the homepage to see both restoration patterns!

## Running the Example

```bash
pnpm install
pnpm dev
```

Navigate between Users and Products routes to see automatic search parameter persistence in action.
12 changes: 12 additions & 0 deletions examples/react/search-persistence/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
24 changes: 24 additions & 0 deletions examples/react/search-persistence/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"name": "tanstack-router-react-example-basic-file-based",
"private": true,
"type": "module",
"scripts": {
"dev": "vite --port 3000",
"build": "vite build && tsc --noEmit",
"serve": "vite preview",
"start": "vite"
},
"dependencies": {
"@tanstack/react-router": "workspace:*",
"@tanstack/react-router-devtools": "workspace:*",
"@tanstack/react-store": "^0.7.0",
"@tanstack/router-plugin": "workspace:*",
"postcss": "^8.5.1",
"autoprefixer": "^10.4.20",
"tailwindcss": "^3.4.17",
"zod": "^3.24.2"
},
"devDependencies": {
"@vitejs/plugin-react": "^4.3.4"
}
}
6 changes: 6 additions & 0 deletions examples/react/search-persistence/postcss.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
29 changes: 29 additions & 0 deletions examples/react/search-persistence/src/main.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { StrictMode } from 'react'
import ReactDOM from 'react-dom/client'
import { RouterProvider, createRouter } from '@tanstack/react-router'
import { routeTree } from './routeTree.gen'
import { setupLocalStorageSync } from './utils/localStorage-sync'
import './styles.css'

// Setup localStorage sync for search persistence (optional)
// if (typeof window !== 'undefined') {
// setupLocalStorageSync()
// }
Comment on lines +5 to +11
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

Resolve ESLint error: remove unused import and prefer guarded dynamic init for the optional localStorage sync

The static import is unused (the call is commented out), which trips unused-imports/no-unused-imports. Remove the import and the commented call. If you want to keep the example, re-introduce it via a client-only dynamic import placed after the router is created.

Apply this diff to clean up the unused pieces:

-import { setupLocalStorageSync } from './utils/localStorage-sync'
@@
-// Setup localStorage sync for search persistence (optional)
-// if (typeof window !== 'undefined') {
-//   setupLocalStorageSync()
-// }

Then, add the guarded dynamic init right after the router is created (see next comment for exact placement):

// Client-only optional localStorage sync for search persistence
if (typeof window !== 'undefined') {
  import('./utils/localStorage-sync')
    .then((m) => m.setupLocalStorageSync())
    .catch(() => {
      // noop: example-only optional feature
    })
}
🧰 Tools
🪛 ESLint

[error] 5-5: 'setupLocalStorageSync' is defined but never used.

(unused-imports/no-unused-imports)

🤖 Prompt for AI Agents
In examples/react/search-persistence/src/main.tsx around lines 5 to 11, remove
the unused static import of setupLocalStorageSync and the commented call to
avoid the unused-imports ESLint error, and instead add a client-only guarded
dynamic import immediately after the router is created: perform a runtime
import('./utils/localStorage-sync') inside an if (typeof window !== 'undefined')
check, then call the module's setupLocalStorageSync in the then handler and
swallow errors in a catch (noop) since this is an optional example feature.


const router = createRouter({ routeTree })

declare module '@tanstack/react-router' {
interface Register {
router: typeof router
}
}

const rootElement = document.getElementById('app')
if (rootElement && !rootElement.innerHTML) {
const root = ReactDOM.createRoot(rootElement)
root.render(
<StrictMode>
<RouterProvider router={router} />
</StrictMode>,
)
}
Loading