-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
feat: add search persistence middleware #5004
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
30f6404
41d598f
fe839f2
27c8222
94082e3
6887082
320ba6e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,292 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
--- | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
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 the following parameters: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
- `persistedSearchParams` (required): Array of search param keys to persist | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
- `exclude` (optional): Array of search param keys to exclude 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 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
**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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Suggested change
🤖 Prompt for AI Agents
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
## 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 name, status, and page | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
middlewares: [persistSearchParams(['name', 'status', 'page'])], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
``` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
```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: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// persist category, minPrice, maxPrice but exclude tempFilter | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
middlewares: [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
persistSearchParams(['category', 'minPrice', 'maxPrice'], ['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: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// persist category and sortOrder, exclude tempFilter and sortBy | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
middlewares: [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
persistSearchParams(['category', 'sortOrder'], ['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 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// Persist category and minPrice, exclude tempFilter and sortBy | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
middlewares: [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
persistSearchParams(['category', 'minPrice'], ['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> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
``` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
## Server-Side Rendering (SSR) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
The search persistence middleware is **SSR-safe** and automatically creates isolated store instances per request to prevent state leakage between users. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
### Key SSR Features | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
- **Per-request isolation**: Each SSR request gets its own `SearchPersistenceStore` instance | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
- **Automatic hydration**: Client seamlessly takes over from server-rendered state | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
- **No global state**: Prevents cross-request contamination in server environments | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
- **Custom store injection**: Integrate with your own persistence backend | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
### Basic SSR Setup | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
```tsx | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import { createRouter, SearchPersistenceStore } from '@tanstack/react-router' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import { routeTree } from './routeTree.gen' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
export function createAppRouter() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// Create isolated store per router instance (per SSR request) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const searchPersistenceStore = | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
typeof window !== 'undefined' ? new SearchPersistenceStore() : undefined | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return createRouter({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
routeTree, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
searchPersistenceStore, // Inject the store | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// ... other options | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
``` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+189
to
+206
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. SSR “Basic Setup” contradicts the per-request isolation claim; fix sample to create the store on the server too. The comment says “per SSR request,” but the code creates a store only on the client. Align the example with the server entry that injects a per-request store. -export function createAppRouter() {
- // Create isolated store per router instance (per SSR request)
- const searchPersistenceStore =
- typeof window !== 'undefined' ? new SearchPersistenceStore() : undefined
-
- return createRouter({
- routeTree,
- searchPersistenceStore, // Inject the store
- // ... other options
- })
-}
+export function createAppRouter() {
+ // Create a new store per router instance.
+ // On the server, ensure this is done per request.
+ const searchPersistenceStore = new SearchPersistenceStore()
+
+ return createRouter({
+ routeTree,
+ searchPersistenceStore, // Inject the store (server: per request; client: per hydration)
+ // ... other options
+ })
+} Follow-up: ensure the server entry (e.g., your 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
### Custom Persistence Backend | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
For production SSR applications, integrate with your own persistence layer: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
```tsx | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import { createRouter, SearchPersistenceStore } from '@tanstack/react-router' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
export function createAppRouter(userId?: string) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
let searchPersistenceStore: SearchPersistenceStore | undefined | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if (typeof window !== 'undefined') { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
searchPersistenceStore = new SearchPersistenceStore() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// Load user's saved searches from your backend | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
loadUserSearches(userId).then((savedSearches) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Object.entries(savedSearches).forEach(([routeId, searchParams]) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
searchPersistenceStore.saveSearch(routeId, searchParams) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// Save changes back to your backend | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
searchPersistenceStore.subscribe(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const state = searchPersistenceStore.state | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
saveUserSearches(userId, state) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return createRouter({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
routeTree, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
searchPersistenceStore, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
``` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
### SSR Considerations | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
- **Client-only store**: Store is only created on client-side (`typeof window !== 'undefined'`) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
- **Hydration-safe**: No server/client mismatch issues | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
- **Performance**: Restored data bypasses validation to prevent SSR timing issues | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
- **Memory efficient**: Stores are garbage collected per request | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+242
to
+248
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Revise “Client-only store” bullet; it conflicts with the server example and per-request isolation. Rephrase to reflect recommended usage: create a new store per SSR request on the server; create one on the client during hydration. -- **Client-only store**: Store is only created on client-side (`typeof window !== 'undefined'`)
- - **Hydration-safe**: No server/client mismatch issues
+- **Per-request server store**: On the server, create a new `SearchPersistenceStore` per request and pass it to the router instance.
+- **Client-side hydration**: On the client, create a `SearchPersistenceStore` during hydration (or reuse one injected via SSR) to continue from server state.
+ - **Hydration-safe**: No server/client mismatch issues 📝 Committable suggestion
Suggested change
🧰 Tools🪛 LanguageTool[grammar] ~244-~244: There might be a mistake here. (QB_NEW_EN) 🤖 Prompt for AI Agents
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
## 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> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
``` |
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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Grammar nit: add article for clarity
“with empty search” → “with an empty search”.
Apply this diff:
📝 Committable suggestion
🤖 Prompt for AI Agents