Skip to content

Commit 30f6404

Browse files
feat: add search persistence middleware
- Add persistSearchParams middleware for automatic search parameter persistence - Add getSearchPersistenceStore() function with full type inference - Include comprehensive example with Users and Products routes - Add API documentation following project conventions - Support selective parameter exclusion with typed arrays - SSR compatible with proper route context handling - Framework agnostic core with React integration
1 parent 3f05c0b commit 30f6404

File tree

22 files changed

+1241
-98
lines changed

22 files changed

+1241
-98
lines changed
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
---
2+
id: persistSearchParams
3+
title: Search middleware to persist search params
4+
---
5+
6+
`persistSearchParams` is a search middleware that automatically saves and restores search parameters when navigating between routes.
7+
8+
## persistSearchParams props
9+
10+
`persistSearchParams` accepts one of the following inputs:
11+
12+
- `undefined` (no arguments): persist all search params
13+
- a list of keys of those search params that shall be excluded from persistence
14+
15+
## How it works
16+
17+
The middleware has two main functions:
18+
19+
1. **Saving**: Automatically saves search parameters when they change
20+
2. **Restoring**: Restores saved parameters when the middleware is triggered with empty search
21+
22+
**Important**: The middleware only runs when search parameters are being processed. This means:
23+
24+
- **Without search prop**: `<Link to="/users">` → Middleware doesn't run → No restoration
25+
- **With search function**: `<Link to="/users" search={(prev) => prev}>` → Middleware runs → Restoration happens
26+
- **With explicit search**: `<Link to="/users" search={{ name: 'John' }}>` → Middleware runs → No restoration (params provided)
27+
28+
## Restoration Behavior
29+
30+
⚠️ **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.
31+
32+
**Recommended**: Always be explicit about restoration intent using the `search` prop.
33+
34+
## Examples
35+
36+
```tsx
37+
import { z } from 'zod'
38+
import { createFileRoute, persistSearchParams } from '@tanstack/react-router'
39+
40+
const usersSearchSchema = z.object({
41+
name: z.string().optional().catch(''),
42+
status: z.enum(['active', 'inactive', 'all']).optional().catch('all'),
43+
page: z.number().optional().catch(0),
44+
})
45+
46+
export const Route = createFileRoute('/users')({
47+
validateSearch: usersSearchSchema,
48+
search: {
49+
// persist all search params
50+
middlewares: [persistSearchParams()],
51+
},
52+
})
53+
```
54+
55+
```tsx
56+
import { z } from 'zod'
57+
import { createFileRoute, persistSearchParams } from '@tanstack/react-router'
58+
59+
const productsSearchSchema = z.object({
60+
category: z.string().optional(),
61+
minPrice: z.number().optional(),
62+
maxPrice: z.number().optional(),
63+
tempFilter: z.string().optional(),
64+
})
65+
66+
export const Route = createFileRoute('/products')({
67+
validateSearch: productsSearchSchema,
68+
search: {
69+
// exclude tempFilter from persistence
70+
middlewares: [persistSearchParams(['tempFilter'])],
71+
},
72+
})
73+
```
74+
75+
```tsx
76+
import { z } from 'zod'
77+
import { createFileRoute, persistSearchParams } from '@tanstack/react-router'
78+
79+
const searchSchema = z.object({
80+
category: z.string().optional(),
81+
sortBy: z.string().optional(),
82+
sortOrder: z.string().optional(),
83+
tempFilter: z.string().optional(),
84+
})
85+
86+
export const Route = createFileRoute('/products')({
87+
validateSearch: searchSchema,
88+
search: {
89+
// exclude tempFilter and sortBy from persistence
90+
middlewares: [persistSearchParams(['tempFilter', 'sortBy'])],
91+
},
92+
})
93+
```
94+
95+
## Restoration Patterns
96+
97+
### Automatic Restoration with Links
98+
99+
Use `search={(prev) => prev}` to trigger middleware restoration:
100+
101+
```tsx
102+
import { Link } from '@tanstack/react-router'
103+
104+
function Navigation() {
105+
return (
106+
<div>
107+
{/* Full restoration - restores all saved parameters */}
108+
<Link to="/users" search={(prev) => prev}>
109+
Users
110+
</Link>
111+
112+
{/* Partial override - restore saved params but override specific ones */}
113+
<Link to="/products" search={(prev) => ({ ...prev, category: 'Electronics' })}>
114+
Electronics Products
115+
</Link>
116+
117+
{/* Clean navigation - no restoration */}
118+
<Link to="/users">
119+
Users (clean slate)
120+
</Link>
121+
</div>
122+
)
123+
}
124+
```
125+
126+
### Exclusion Strategies
127+
128+
You have two ways to exclude parameters from persistence:
129+
130+
**1. Middleware-level exclusion** (permanent):
131+
```tsx
132+
// These parameters are never saved
133+
middlewares: [persistSearchParams(['tempFilter', 'sortBy'])]
134+
```
135+
136+
**2. Link-level exclusion** (per navigation):
137+
```tsx
138+
// Restore saved params but exclude specific ones
139+
<Link to="/products" search={(prev) => {
140+
const { tempFilter, ...rest } = prev || {}
141+
return rest
142+
}}>
143+
Products (excluding temp filter)
144+
</Link>
145+
```
146+
147+
### Manual Restoration
148+
149+
Access the store directly for full control:
150+
151+
```tsx
152+
import { getSearchPersistenceStore, Link } from '@tanstack/react-router'
153+
154+
function CustomNavigation() {
155+
const store = getSearchPersistenceStore()
156+
const savedUsersSearch = store.getSearch('/users')
157+
158+
return (
159+
<Link to="/users" search={savedUsersSearch || {}}>
160+
Users (with saved search)
161+
</Link>
162+
)
163+
}
164+
```
165+
166+
## Using the search persistence store
167+
168+
You can also access the search persistence store directly for manual control:
169+
170+
```tsx
171+
import { getSearchPersistenceStore } from '@tanstack/react-router'
172+
173+
// Get the fully typed store instance
174+
const store = getSearchPersistenceStore()
175+
176+
// Get persisted search for a route
177+
const savedSearch = store.getSearch('/users')
178+
179+
// Clear persisted search for a specific route
180+
store.clearSearch('/users')
181+
182+
// Clear all persisted searches
183+
store.clearAllSearches()
184+
185+
// Manually save search for a route
186+
store.saveSearch('/users', { name: 'John', status: 'active' })
187+
```
188+
189+
```tsx
190+
import { getSearchPersistenceStore } from '@tanstack/react-router'
191+
import { useStore } from '@tanstack/react-store'
192+
import React from 'react'
193+
194+
function MyComponent() {
195+
const store = getSearchPersistenceStore()
196+
const storeState = useStore(store.store)
197+
198+
const clearUserSearch = () => {
199+
store.clearSearch('/users')
200+
}
201+
202+
return (
203+
<div>
204+
<p>Saved search: {JSON.stringify(storeState['/users'])}</p>
205+
<button onClick={clearUserSearch}>Clear saved search</button>
206+
</div>
207+
)
208+
}
209+
```
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
# Search Persistence Example
2+
3+
This example demonstrates TanStack Router's search persistence middleware, which automatically saves and restores search parameters when navigating between routes.
4+
5+
## Overview
6+
7+
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.
8+
9+
## Key Features
10+
11+
- **Automatic Persistence**: Search parameters are saved/restored automatically
12+
- **Selective Exclusion**: Choose which parameters to exclude from persistence
13+
- **Type Safety**: Full TypeScript support with automatic type inference
14+
- **Manual Control**: Direct store access for advanced use cases
15+
16+
## Basic Usage
17+
18+
```tsx
19+
import { createFileRoute, persistSearchParams } from '@tanstack/react-router'
20+
21+
// Persist all search parameters
22+
export const Route = createFileRoute('/users')({
23+
validateSearch: usersSearchSchema,
24+
search: {
25+
middlewares: [persistSearchParams()],
26+
},
27+
})
28+
29+
// Exclude specific parameters from persistence
30+
export const Route = createFileRoute('/products')({
31+
validateSearch: productsSearchSchema,
32+
search: {
33+
middlewares: [persistSearchParams(['tempFilter', 'sortBy'])],
34+
},
35+
})
36+
```
37+
38+
## Restoration Patterns
39+
40+
⚠️ **Important**: The middleware only runs when search parameters are being processed. Always be explicit about your restoration intent.
41+
42+
### Automatic Restoration
43+
```tsx
44+
import { Link } from '@tanstack/react-router'
45+
46+
// Full restoration - restores all saved parameters
47+
<Link to="/users" search={(prev) => prev}>
48+
Users (restore all)
49+
</Link>
50+
51+
// Partial override - restore but override specific parameters
52+
<Link to="/products" search={(prev) => ({ ...prev, category: 'Electronics' })}>
53+
Electronics Products
54+
</Link>
55+
56+
// Clean navigation - no restoration
57+
<Link to="/users">
58+
Users (clean slate)
59+
</Link>
60+
```
61+
62+
### Manual Restoration
63+
Access the store directly for full control:
64+
65+
```tsx
66+
import { getSearchPersistenceStore } from '@tanstack/react-router'
67+
68+
const store = getSearchPersistenceStore()
69+
const savedSearch = store.getSearch('/users')
70+
71+
<Link to="/users" search={savedSearch || {}}>
72+
Users (manual restoration)
73+
</Link>
74+
```
75+
76+
### ⚠️ Unexpected Behavior Warning
77+
78+
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.
79+
80+
**Recommended**: Always use the `search` prop to be explicit about restoration intent.
81+
82+
## Try It
83+
84+
1. Navigate to `/users` and search for a name
85+
2. Navigate to `/products` and set some filters
86+
3. Use the test links on the homepage to see both restoration patterns!
87+
88+
## Running the Example
89+
90+
```bash
91+
pnpm install
92+
pnpm dev
93+
```
94+
95+
Navigate between Users and Products routes to see automatic search parameter persistence in action.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<title>Vite App</title>
7+
</head>
8+
<body>
9+
<div id="app"></div>
10+
<script type="module" src="/src/main.tsx"></script>
11+
</body>
12+
</html>
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"name": "tanstack-router-react-example-basic-file-based",
3+
"private": true,
4+
"type": "module",
5+
"scripts": {
6+
"dev": "vite --port 3000",
7+
"build": "vite build && tsc --noEmit",
8+
"serve": "vite preview",
9+
"start": "vite"
10+
},
11+
"dependencies": {
12+
"@tanstack/react-router": "workspace:*",
13+
"@tanstack/react-router-devtools": "workspace:*",
14+
"@tanstack/react-store": "^0.7.0",
15+
"@tanstack/router-plugin": "workspace:*",
16+
"postcss": "^8.5.1",
17+
"autoprefixer": "^10.4.20",
18+
"tailwindcss": "^3.4.17",
19+
"zod": "^3.24.2"
20+
},
21+
"devDependencies": {
22+
"@vitejs/plugin-react": "^4.3.4"
23+
}
24+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export default {
2+
plugins: {
3+
tailwindcss: {},
4+
autoprefixer: {},
5+
},
6+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { StrictMode } from 'react'
2+
import ReactDOM from 'react-dom/client'
3+
import { RouterProvider, createRouter } from '@tanstack/react-router'
4+
import { routeTree } from './routeTree.gen'
5+
import { setupLocalStorageSync } from './utils/localStorage-sync'
6+
import './styles.css'
7+
8+
// Setup localStorage sync for search persistence (optional)
9+
if (typeof window !== 'undefined') {
10+
setupLocalStorageSync()
11+
}
12+
13+
const router = createRouter({ routeTree })
14+
15+
declare module '@tanstack/react-router' {
16+
interface Register {
17+
router: typeof router
18+
}
19+
}
20+
21+
const rootElement = document.getElementById('app')
22+
if (rootElement && !rootElement.innerHTML) {
23+
const root = ReactDOM.createRoot(rootElement)
24+
root.render(
25+
<StrictMode>
26+
<RouterProvider router={router} />
27+
</StrictMode>,
28+
)
29+
}

0 commit comments

Comments
 (0)