-
-
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?
feat: add search persistence middleware #5004
Conversation
- 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
WalkthroughAdds route-aware search-parameter persistence: a persistSearchParams middleware, a typed SearchPersistenceStore with accessor, wiring of route metadata into search middleware execution, re-exports in React/core barrels, and a complete React example + docs demonstrating usage, exclusions, restoration, and optional localStorage sync. (50 words) Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant UI as Route Component
participant Router as Router
participant MW as persistSearchParams (Middleware)
participant Store as SearchPersistenceStore
Note over Router,MW: Middleware receives route metadata {id, fullPath}
UI->>Router: navigate(to, search?)
Router->>MW: invoke({ search, next, route })
alt incoming search is empty
MW->>Store: getSearch(route.id)
Store-->>MW: savedSearch|null
MW->>MW: use savedSearch if present
end
MW->>Router: next(merged/filteredSearch)
MW->>Store: saveSearch(route.id, filteredSearch) -- when non-empty
Router-->>UI: render with finalSearch
sequenceDiagram
autonumber
participant App as App boot
participant Store as SearchPersistenceStore
participant LS as localStorage
App->>LS: getItem('search-persistence')
LS-->>App: serialized state
App->>Store: saveSearch(routeId, search) per entry
Store-->>App: notify subscribers
App->>LS: setItem('search-persistence', serialized state) on updates
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related issues
Poem
Tip 🔌 Remote MCP (Model Context Protocol) integration is now available!Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats. ✨ Finishing Touches
🧪 Generate unit tests
🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
- Run prettier:write to format all files according to project standards - Fix ESLint import sorting and method signature style issues - All files now pass prettier and eslint checks
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.
Actionable comments posted: 8
🧹 Nitpick comments (31)
examples/react/search-persistence/tsconfig.json (1)
2-8
: Consider adding Vite-friendly compiler options (moduleResolution, types, noEmit)These improve DX (env typing) and align with modern Vite setups.
Apply this diff:
"compilerOptions": { "strict": true, "esModuleInterop": true, "jsx": "react-jsx", "lib": ["DOM", "DOM.Iterable", "ES2022"], - "skipLibCheck": true + "skipLibCheck": true, + "moduleResolution": "bundler", + "module": "ESNext", + "types": ["vite/client"], + "noEmit": true }examples/react/search-persistence/README.md (4)
62-64
: Remove trailing spaces in the headingMinor Markdown cleanup flagged by tooling; trailing spaces aren’t needed after a heading.
Apply this diff:
-### Manual Restoration +### Manual Restoration
68-74
: Prefer preserving current search when no persisted state existsUsing an empty object may unintentionally reset a route’s search. Falling back to prev keeps the current state intact if nothing is persisted.
Apply this diff:
-const store = getSearchPersistenceStore() -const savedSearch = store.getSearch('/users') - -<Link to="/users" search={savedSearch || {}}> - Users (manual restoration) -</Link> +const store = getSearchPersistenceStore() +const savedSearch = store.getSearch('/users') + +<Link + to="/users" + // If nothing is persisted, keep the current search untouched instead of forcing {} + search={(prev) => savedSearch ?? prev} +> + Users (manual restoration) +</Link>
75-76
: Consider adding a quick example for clearing persisted searchYou mention manual control; a “clear” example rounds it out.
Apply this diff:
+ +### Clearing persisted search + +```tsx +import { getSearchPersistenceStore } from '@tanstack/react-router' +const store = getSearchPersistenceStore() +// Clear only the Users route persisted search +store.clearSearch('/users') +// Or clear everything +store.clearAllSearches() +```
95-95
: Minor grammar tweak for readabilityAdd “the” and simplify wording.
Apply this diff:
-Navigate between Users and Products routes to see automatic search parameter persistence in action. +Navigate between the Users and Products routes to see search parameter persistence in action.packages/router-core/src/route.ts (1)
74-78
: StrengthenSearchMiddlewareContext.route
typingThe router already threads the route object (with
id
andfullPath
) through the middleware pipeline inrouter.ts
, so no changes are needed there. To enforce immutability and reuse the existingRouteExtensions
interface, update the context type as follows:export type SearchMiddlewareContext<TSearchSchema> = { search: TSearchSchema next: (newSearch: TSearchSchema) => TSearchSchema - route: { id: string; fullPath: string } + route: Readonly<Pick<RouteExtensions<string, string>, 'id' | 'fullPath'>> }This ensures middlewares see a readonly, consistently typed route object.
examples/react/search-persistence/index.html (1)
1-12
: Use a descriptive title for the exampleMinor UX nit: “Vite App” is generic. Consider a descriptive title to help users distinguish tabs.
Apply this diff:
- <title>Vite App</title> + <title>Search Persistence – TanStack Router</title>examples/react/search-persistence/src/styles.css (1)
8-10
: Global border color via universal selector: confirm intentional scopeStyling all elements’ border color with
*
is a common pattern but can be heavy on very large DOMs and also affects elements that don’t render borders. If the intent is to set the default border color theme-wide, this is fine; otherwise consider scoping to a container or adding::before, ::after
if pseudo-elements are relied upon for borders.Would you like a scoped alternative (e.g., a wrapper class) to reduce global impact?
examples/react/search-persistence/src/main.tsx (1)
8-11
: Avoid duplicate subscriptions during HMR by disposing the localStorage sync listenerIn dev, Vite HMR will re-run module code and stack multiple subscriptions. Dispose the previous subscription to prevent duplicate writes and memory leaks.
-// Setup localStorage sync for search persistence (optional) -if (typeof window !== 'undefined') { - setupLocalStorageSync() -} +// Setup localStorage sync for search persistence (optional) +if (typeof window !== 'undefined') { + const unsubscribe = setupLocalStorageSync() + // Avoid duplicate subscriptions during Vite HMR in dev + if (import.meta?.hot) { + import.meta.hot.dispose(() => unsubscribe()) + } +}examples/react/search-persistence/src/utils/localStorage-sync.ts (2)
13-15
: Filter unknown route IDs when restoring persisted stateWithout guarding, arbitrary keys in localStorage can pollute the store. Filter restores to known route IDs.
- Object.entries(parsedState).forEach(([routeId, search]) => { - store.saveSearch(routeId as any, search as Record<string, unknown>) - }) + Object.entries(parsedState).forEach(([routeId, search]) => { + if (routeId in store.state) { + store.saveSearch(routeId as any, search as Record<string, unknown>) + } + })
21-28
: Debounce localStorage writes to reduce churn and quota-pressureSearch changes can happen quickly (typing, sliders). Debounce writes to avoid frequent syncs.
- // Subscribe to changes and sync to localStorage - return store.subscribe(() => { - try { - localStorage.setItem(STORAGE_KEY, JSON.stringify(store.state)) - } catch (error) { - console.warn('Failed to sync search persistence to localStorage:', error) - } - }) + // Subscribe to changes and sync to localStorage (debounced to avoid frequent writes) + let writeTimer: number | undefined + return store.subscribe(() => { + if (writeTimer) { + clearTimeout(writeTimer) + } + writeTimer = window.setTimeout(() => { + try { + localStorage.setItem(STORAGE_KEY, JSON.stringify(store.state)) + } catch (error) { + console.warn('Failed to sync search persistence to localStorage:', error) + } + }, 100) + })examples/react/search-persistence/package.json (1)
2-2
: Rename the example to reflect its purposeThe current name suggests a “basic file-based” example. Consider renaming to improve discoverability.
- "name": "tanstack-router-react-example-basic-file-based", + "name": "tanstack-router-react-example-search-persistence",examples/react/search-persistence/src/routes/__root.tsx (2)
for the primary navigation
12-12
: Use semanticSmall a11y improvement; helps assistive tech and semantics.
- <div className="p-2 flex gap-2 text-lg border-b"> + <nav className="p-2 flex gap-2 text-lg border-b"> ... - </div> + </nav>Also applies to: 40-40
43-43
: Render devtools only in development to keep prod bundles leanGate the devtools behind a dev check.
- <TanStackRouterDevtools /> + {import.meta.env.DEV && <TanStackRouterDevtools />}examples/react/search-persistence/src/routes/index.tsx (1)
1-1
: Fix ESLint sort-imports: sort named specifiersComplies with the configured sort-imports rule.
-import { createFileRoute, Link } from '@tanstack/react-router' +import { Link, createFileRoute } from '@tanstack/react-router'examples/react/search-persistence/src/routes/users.tsx (4)
1-1
: Sort imported members to satisfy lint ruleESLint reports sort-imports. Alphabetize the named imports.
-import { createFileRoute, useNavigate, persistSearchParams } from '@tanstack/react-router' +import { createFileRoute, persistSearchParams, useNavigate } from '@tanstack/react-router'
5-10
: Coerce numeric search params from the URLQuery-string values arrive as strings. Using z.coerce.number() makes validation robust across reloads/deep-links.
- page: z.number().optional().catch(0), - limit: z.number().optional().catch(10), + page: z.coerce.number().optional().catch(0), + limit: z.coerce.number().optional().catch(10),
59-63
: Avoidas any
in navigate; keep types intactLet inference work by returning a properly typed object or cast the result, not the whole navigate call.
- const updateSearch = (updates: Partial<UsersSearchSchema>) => { - navigate({ - search: (prev: UsersSearchSchema) => ({ ...prev, ...updates, page: 0 }), - } as any) - } + const updateSearch = (updates: Partial<UsersSearchSchema>) => { + navigate({ + search: (prev: UsersSearchSchema) => + ({ ...prev, ...updates, page: 0 }) as UsersSearchSchema, + }) + }
80-82
: Tighten typing for status changeAvoid
as any
. Narrow to the enum type.- onChange={(e) => updateSearch({ status: e.target.value as any })} + onChange={(e) => + updateSearch({ status: e.target.value as UsersSearchSchema['status'] }) + }docs/router/framework/react/api/router/persistSearchParamsFunction.md (2)
24-27
: Clarify and fix grammar in trigger rules; call out empty-search restorationImprove readability and explicitly mention that an explicit empty object restores.
-- **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) +- Without a `search` prop: `<Link to="/users">` → Middleware does not run → No restoration +- With a search function: `<Link to="/users" search={(prev) => prev}>` → Middleware runs → Restoration happens +- With explicit non-empty search: `<Link to="/users" search={{ name: 'John' }}>` → Middleware runs → No restoration (params provided) +- With explicit empty search: `<Link to="/users" search={{}}> ` → Middleware runs → Restoration happens (empty search is treated as “restore”)
147-165
: Add a “clean reset” example that clears persistenceSince
{}
restores, show how to clear the store before navigating for a true reset.## Manual Restoration @@ function CustomNavigation() { const store = getSearchPersistenceStore() const savedUsersSearch = store.getSearch('/users') return ( <Link to="/users" search={savedUsersSearch || {}}> Users (with saved search) </Link> ) } + +// Clean reset (clear persisted state, then navigate with empty search) +function ResetUsers() { + const store = getSearchPersistenceStore() + return ( + <Link + to="/users" + search={() => { + store.clearSearch('/users') + return {} + }} + > + Reset Users (clean) + </Link> + ) +}examples/react/search-persistence/src/routes/products.tsx (4)
1-1
: Sort imported members and drop trailing comma for lint cleanlinessAlphabetize members and remove the dangling comma inside the import specifier.
-import { createFileRoute, useNavigate, persistSearchParams, } from '@tanstack/react-router' +import { createFileRoute, persistSearchParams, useNavigate } from '@tanstack/react-router'
5-10
: Coerce numeric search params from the URLUse z.coerce.number() so deep-links and reloads with numeric strings validate correctly.
- minPrice: z.number().optional().catch(0), - maxPrice: z.number().optional().catch(1000), + minPrice: z.coerce.number().optional().catch(0), + maxPrice: z.coerce.number().optional().catch(1000),
58-62
: Avoidas any
in navigate; keep types intactPrefer casting the produced object or rely on inference.
- const updateSearch = (updates: Partial<ProductsSearchSchema>) => { - navigate({ - search: (prev: ProductsSearchSchema) => ({ ...prev, ...updates }), - } as any) - } + const updateSearch = (updates: Partial<ProductsSearchSchema>) => { + navigate({ + search: (prev: ProductsSearchSchema) => + ({ ...prev, ...updates }) as ProductsSearchSchema, + }) + }
104-107
: Tighten typing for sortBy changeAvoid
as any
. Narrow to the enum union type.- onChange={(e) => updateSearch({ sortBy: e.target.value as any })} + onChange={(e) => + updateSearch({ sortBy: e.target.value as ProductsSearchSchema['sortBy'] }) + }packages/router-core/src/router.ts (1)
2758-2775
: Potential double validation of searchSearch validation is also performed earlier in buildLocation when strict mode is on. Running another validate middleware here duplicates work. Consider gating this middleware behind a flag or removing the earlier strict-block to avoid redundant compute.
packages/router-core/src/searchMiddleware.ts (5)
4-4
: Sort imported members to satisfy lint rule (sort-imports).Swap the named members so they are alphabetically ordered.
-import type { SearchMiddleware, AnyRoute } from './route' +import type { AnyRoute, SearchMiddleware } from './route'
6-6
: Sort imported members to satisfy lint rule (sort-imports).Order the named imports alphabetically.
-import type { RoutesById, RouteById } from './routeInfo' +import type { RouteById, RoutesById } from './routeInfo'
116-126
: Potential TS inconsistency: cast routeId to string for symmetry and safety.You already cast to
string
inclearSearch
; do the same here to avoid generic key issues in destructuring.- if (Object.keys(cleanedSearch).length === 0) { - const { [routeId]: _, ...rest } = prevState + if (Object.keys(cleanedSearch).length === 0) { + const { [routeId as string]: _, ...rest } = prevState return rest }
161-179
: Fix method-signature lint errors in return type (method-signature-style).Convert method signatures to function properties in the returned type to satisfy the lint rule.
subscribe: (listener: () => void) => () => void - getSearch<TRouteId extends keyof RoutesById<TRouteTree>>( - routeId: TRouteId, - ): RouteById<TRouteTree, TRouteId>['types']['fullSearchSchema'] | null - saveSearch<TRouteId extends keyof RoutesById<TRouteTree>>( - routeId: TRouteId, - search: RouteById<TRouteTree, TRouteId>['types']['fullSearchSchema'], - ): void - clearSearch<TRouteId extends keyof RoutesById<TRouteTree>>( - routeId: TRouteId, - ): void - clearAllSearches(): void + getSearch: <TRouteId extends keyof RoutesById<TRouteTree>>( + routeId: TRouteId, + ) => RouteById<TRouteTree, TRouteId>['types']['fullSearchSchema'] | null + saveSearch: <TRouteId extends keyof RoutesById<TRouteTree>>( + routeId: TRouteId, + search: RouteById<TRouteTree, TRouteId>['types']['fullSearchSchema'], + ) => void + clearSearch: <TRouteId extends keyof RoutesById<TRouteTree>>( + routeId: TRouteId, + ) => void + clearAllSearches: () => void
200-241
: Optional: centralize “empty-ish” checks for maintainability.The empty-value checks are duplicated in both the middleware and store. Consider extracting a tiny internal util (e.g., isEmptyish) and reusing it. Not a blocker.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (22)
docs/router/framework/react/api/router/persistSearchParamsFunction.md
(1 hunks)examples/react/search-persistence/README.md
(1 hunks)examples/react/search-persistence/index.html
(1 hunks)examples/react/search-persistence/package.json
(1 hunks)examples/react/search-persistence/postcss.config.mjs
(1 hunks)examples/react/search-persistence/src/main.tsx
(1 hunks)examples/react/search-persistence/src/routeTree.gen.ts
(1 hunks)examples/react/search-persistence/src/routes/__root.tsx
(1 hunks)examples/react/search-persistence/src/routes/index.tsx
(1 hunks)examples/react/search-persistence/src/routes/products.tsx
(1 hunks)examples/react/search-persistence/src/routes/users.tsx
(1 hunks)examples/react/search-persistence/src/styles.css
(1 hunks)examples/react/search-persistence/src/type-inference-test.ts
(1 hunks)examples/react/search-persistence/src/utils/localStorage-sync.ts
(1 hunks)examples/react/search-persistence/tailwind.config.mjs
(1 hunks)examples/react/search-persistence/tsconfig.json
(1 hunks)examples/react/search-persistence/vite.config.js
(1 hunks)packages/react-router/src/index.tsx
(2 hunks)packages/router-core/src/index.ts
(1 hunks)packages/router-core/src/route.ts
(1 hunks)packages/router-core/src/router.ts
(12 hunks)packages/router-core/src/searchMiddleware.ts
(3 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (10)
examples/react/search-persistence/src/routes/index.tsx (3)
examples/react/search-persistence/src/routes/__root.tsx (1)
Route
(5-7)examples/react/search-persistence/src/routes/products.tsx (1)
Route
(14-22)examples/react/search-persistence/src/routes/users.tsx (1)
Route
(14-22)
examples/react/search-persistence/src/utils/localStorage-sync.ts (1)
packages/router-core/src/searchMiddleware.ts (3)
store
(74-76)store
(184-186)getSearchPersistenceStore
(158-198)
examples/react/search-persistence/src/routes/__root.tsx (5)
examples/react/search-persistence/src/routes/index.tsx (1)
Route
(3-5)examples/react/search-persistence/src/routes/products.tsx (1)
Route
(14-22)examples/react/search-persistence/src/routes/users.tsx (1)
Route
(14-22)packages/react-router/src/index.tsx (4)
Route
(263-263)createRootRoute
(267-267)Link
(156-156)Outlet
(252-252)packages/react-router-devtools/src/TanStackRouterDevtools.tsx (1)
TanStackRouterDevtools
(46-126)
examples/react/search-persistence/src/main.tsx (2)
examples/react/search-persistence/src/utils/localStorage-sync.ts (1)
setupLocalStorageSync
(5-29)packages/router-core/src/router.ts (1)
Register
(99-101)
examples/react/search-persistence/src/routes/users.tsx (2)
examples/react/search-persistence/src/routes/products.tsx (1)
Route
(14-22)packages/router-core/src/searchMiddleware.ts (1)
persistSearchParams
(200-242)
packages/router-core/src/searchMiddleware.ts (3)
packages/router-core/src/route.ts (2)
AnyRoute
(703-718)SearchMiddleware
(80-82)packages/router-core/src/router.ts (1)
RegisteredRouter
(103-103)packages/router-core/src/utils.ts (1)
replaceEqualDeep
(212-259)
examples/react/search-persistence/src/routes/products.tsx (2)
examples/react/search-persistence/src/routes/users.tsx (1)
Route
(14-22)packages/router-core/src/searchMiddleware.ts (1)
persistSearchParams
(200-242)
packages/router-core/src/router.ts (2)
packages/router-core/src/route.ts (2)
RouteContextOptions
(936-944)SearchMiddleware
(80-82)packages/router-core/src/utils.ts (1)
functionalUpdate
(195-204)
examples/react/search-persistence/src/type-inference-test.ts (3)
packages/router-core/src/searchMiddleware.ts (5)
store
(74-76)store
(184-186)getSearchPersistenceStore
(158-198)state
(66-68)state
(181-183)packages/react-router/src/index.tsx (1)
getSearchPersistenceStore
(36-36)packages/router-core/src/index.ts (1)
getSearchPersistenceStore
(261-261)
examples/react/search-persistence/src/routeTree.gen.ts (2)
packages/react-router/src/index.tsx (2)
FileRouteTypes
(174-174)FileRoutesByPath
(116-116)packages/router-core/src/index.ts (2)
FileRouteTypes
(64-64)FileRoutesByPath
(65-65)
🪛 ESLint
examples/react/search-persistence/src/routes/index.tsx
[error] 1-1: Member 'Link' of the import declaration should be sorted alphabetically.
(sort-imports)
examples/react/search-persistence/src/routes/users.tsx
[error] 1-1: Member 'persistSearchParams' of the import declaration should be sorted alphabetically.
(sort-imports)
packages/router-core/src/searchMiddleware.ts
[error] 4-4: Member 'AnyRoute' of the import declaration should be sorted alphabetically.
(sort-imports)
[error] 6-6: Member 'RouteById' of the import declaration should be sorted alphabetically.
(sort-imports)
[error] 168-170: Shorthand method signature is forbidden. Use a function property instead.
(@typescript-eslint/method-signature-style)
[error] 171-174: Shorthand method signature is forbidden. Use a function property instead.
(@typescript-eslint/method-signature-style)
[error] 175-177: Shorthand method signature is forbidden. Use a function property instead.
(@typescript-eslint/method-signature-style)
[error] 178-178: Shorthand method signature is forbidden. Use a function property instead.
(@typescript-eslint/method-signature-style)
examples/react/search-persistence/src/routes/products.tsx
[error] 1-1: Member 'persistSearchParams' of the import declaration should be sorted alphabetically.
(sort-imports)
🪛 LanguageTool
examples/react/search-persistence/README.md
[grammar] ~62-~62: There might be a mistake here.
Context: ...ate) ``` ### Manual Restoration Access the store directly for full contr...
(QB_NEW_EN)
[grammar] ~95-~95: There might be a mistake here.
Context: ...ash pnpm install pnpm dev ``` Navigate between Users and Products routes to see automa...
(QB_NEW_EN)
docs/router/framework/react/api/router/persistSearchParamsFunction.md
[grammar] ~24-~24: There might be a mistake here.
Context: ... Middleware doesn't run → No restoration - With search function: `<Link to="/user...
(QB_NEW_EN)
[grammar] ~25-~25: There might be a mistake here.
Context: ... → Middleware runs → Restoration happens - With explicit search: `<Link to="/user...
(QB_NEW_EN)
🔇 Additional comments (24)
examples/react/search-persistence/tsconfig.json (1)
2-8
: Solid TS defaults for a Vite + React exampleStrict mode, DOM libs, and esModuleInterop look good here.
examples/react/search-persistence/tailwind.config.mjs (1)
1-4
: LGTMMinimal Tailwind config with correct content globs for Vite projects.
examples/react/search-persistence/postcss.config.mjs (1)
1-6
: LGTMPostCSS + Tailwind + Autoprefixer default export is correct for ESM config.
examples/react/search-persistence/vite.config.js (2)
1-14
: Plugin order and options look correctRouter plugin first, then React plugin, with code-splitting enabled. Good.
1-14
: Router and React Vite plugins are declaredThe example’s package.json already includes the required plugins:
- examples/react/search-persistence/package.json
•"@tanstack/router-plugin"
in dependencies
•"@vitejs/plugin-react"
in devDependenciesNo changes needed.
examples/react/search-persistence/README.md (1)
1-15
: Clear, action-oriented READMENice coverage of features, usage, and caveats. This will help users adopt the middleware quickly.
packages/react-router/src/index.tsx (1)
35-37
: Verified: core barrel exports persistence APIsConfirmed that packages/router-core/src/index.ts includes:
- persistSearchParams
- getSearchPersistenceStore
as named re-exports from
./searchMiddleware
. The React barrel’s re-exports are valid—no changes needed.packages/router-core/src/index.ts (1)
257-262
: Search persistence exports verified: no duplicates, implementations found
Confirmed thatpackages/router-core/src/index.ts
does not re-export any of these identifiers more than once, andpackages/router-core/src/searchMiddleware.ts
defines and exportsretainSearchParams
,stripSearchParams
,persistSearchParams
, andgetSearchPersistenceStore
. No further action required.examples/react/search-persistence/src/main.tsx (2)
15-19
: Type registration for the router is correctly declaredThe module augmentation pattern for Register looks correct and should fully type getSearchPersistenceStore for this app.
21-29
: If you later SSR this example, consider hydrateRootThe conditional mount is fine for CSR. For SSR, you’ll need to swap to hydrateRoot and remove the innerHTML check to hydrate existing markup.
Would you like me to provide a CSR/SSR toggle snippet showing createRoot vs hydrateRoot?
examples/react/search-persistence/src/utils/localStorage-sync.ts (1)
1-1
: Confirm getSearchPersistenceStore is re-exported from @tanstack/react-routerI wasn’t able to locate a re-export of
getSearchPersistenceStore
in the router package’s entrypoint. Please verify that the main export file (e.g.src/index.ts
) of@tanstack/react-router
includes:export { getSearchPersistenceStore } from '@tanstack/router-core'so that your import will resolve correctly at compile/runtime.
examples/react/search-persistence/package.json (1)
11-20
: Add react and react-dom to dependencies to ensure the example is runnable standaloneMonorepo hoisting may mask this locally, but adding explicit deps prevents resolution issues for consumers running the example directly.
"dependencies": { "@tanstack/react-router": "workspace:*", "@tanstack/react-router-devtools": "workspace:*", "@tanstack/react-store": "^0.7.0", "@tanstack/router-plugin": "workspace:*", + "react": "^18.3.1", + "react-dom": "^18.3.1", "postcss": "^8.5.1", "autoprefixer": "^10.4.20", "tailwindcss": "^3.4.17", "zod": "^3.24.2" },If you intentionally rely on workspace root deps, feel free to ignore.
examples/react/search-persistence/src/routes/index.tsx (1)
16-32
: Nice demonstration of full restoration via Link.searchUsing search={(prev) => prev} clearly shows how persisted state can be restored across routes.
packages/router-core/src/router.ts (2)
1455-1455
: Dev-only warning is fineThe added console.warn for invalid from paths is helpful during development and doesn’t affect prod builds.
2707-2788
: Route-aware middleware plumbing is well-structuredCollecting middlewares per-route and passing route context enables features like search persistence without breaking legacy filters.
examples/react/search-persistence/src/type-inference-test.ts (1)
5-28
: Type inference API looks solidNice demonstration of per-route typing across getSearch/save/clear and store usage. This will help consumers understand the ergonomics.
docs/router/framework/react/api/router/persistSearchParamsFunction.md (2)
30-33
: Strong recommendation to be explicit about restoration is goodThe guidance to always specify restoration intent will save users from surprises later. Looks great.
166-209
: Store API examples are clear and usefulGood coverage of get/save/clear patterns and integrating with useStore.
examples/react/search-persistence/src/routes/users.tsx (2)
43-57
: useMemo dependencies are sufficientFiltering logic only depends on name and status; memoization scope is correct.
14-22
: Middleware usage matches core implementationpersistSearchParams() without exclusions is appropriate for this route.
examples/react/search-persistence/src/routes/products.tsx (2)
37-56
: Sorting and filtering pipeline is clear and correctCloning before sorting prevents mutating the source; predicates look good.
14-22
: Excluding sortBy from persistence is a good defaultKeeps sorting ephemeral while other filters persist. Nicely demonstrates the API.
examples/react/search-persistence/src/routeTree.gen.ts (1)
7-10
: Generated file looks correct and consistent with Router expectations.Route IDs, paths, module augmentation, and the
routeTree
construction all look good for the example. Since this is auto-generated with lint/TS checks disabled, no further action is needed here.packages/router-core/src/searchMiddleware.ts (1)
37-56
: Casting results in stripSearchParams improves type safety. LGTM.The explicit casts on empty and final results keep the middleware signature sound without over-constraining the implementation.
<button | ||
type="button" | ||
onClick={() => navigate({ search: {} } as any)} | ||
className="border p-2 rounded" | ||
> | ||
Reset | ||
</button> |
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.
🛠️ Refactor suggestion
Reset button will immediately restore persisted search
Because of persistence, {}
restores. Clear the persisted entry first.
- <button
- type="button"
- onClick={() => navigate({ search: {} } as any)}
+ <button
+ type="button"
+ onClick={() => {
+ getSearchPersistenceStore().clearSearch('/products')
+ navigate({ search: {} })
+ }}
className="border p-2 rounded"
>
Reset
</button>
Also add the import at the top:
-import { createFileRoute, persistSearchParams, useNavigate } from '@tanstack/react-router'
+import { createFileRoute, getSearchPersistenceStore, persistSearchParams, useNavigate } from '@tanstack/react-router'
📝 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.
<button | |
type="button" | |
onClick={() => navigate({ search: {} } as any)} | |
className="border p-2 rounded" | |
> | |
Reset | |
</button> | |
// at the top of examples/react/search-persistence/src/routes/products.tsx | |
import { createFileRoute, getSearchPersistenceStore, persistSearchParams, useNavigate } from '@tanstack/react-router' | |
// … later, in your JSX render: | |
<button | |
type="button" | |
onClick={() => { | |
getSearchPersistenceStore().clearSearch('/products') | |
navigate({ search: {} }) | |
}} | |
className="border p-2 rounded" | |
> | |
Reset | |
</button> |
🤖 Prompt for AI Agents
In examples/react/search-persistence/src/routes/products.tsx around lines
114–120, the Reset button currently calls navigate({ search: {} }) which will
immediately restore the persisted search; before navigating, clear the persisted
search entry (for example by calling the project's persistence clear helper or
removing the specific localStorage/sessionStorage key used for persisted search)
and then call navigate to an empty search. Also add the appropriate import at
the top of the file for the persistence clear helper (e.g., import {
clearPersistedSearch } from the module where persistence helpers live) and use
that helper in the button onClick before navigate.
<button | ||
type="button" | ||
onClick={() => navigate({ search: {} } as any)} | ||
className="border p-2 rounded" | ||
> | ||
Reset | ||
</button> |
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.
🛠️ Refactor suggestion
Reset button will immediately restore persisted search
With persistSearchParams, navigating with an empty search {}
triggers restoration. Clear the persisted entry first to truly reset.
-import React from 'react'
+import React from 'react'
+import { getSearchPersistenceStore } from '@tanstack/react-router'
@@
- <button
- type="button"
- onClick={() => navigate({ search: {} } as any)}
+ <button
+ type="button"
+ onClick={() => {
+ getSearchPersistenceStore().clearSearch('/users')
+ navigate({ search: {} })
+ }}
className="border p-2 rounded"
>
Reset
</button>
📝 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.
<button | |
type="button" | |
onClick={() => navigate({ search: {} } as any)} | |
className="border p-2 rounded" | |
> | |
Reset | |
</button> | |
import React from 'react' | |
import { getSearchPersistenceStore } from '@tanstack/react-router' | |
<button | |
type="button" | |
onClick={() => { | |
getSearchPersistenceStore().clearSearch('/users') | |
navigate({ search: {} }) | |
}} | |
className="border p-2 rounded" | |
> | |
Reset | |
</button> |
🤖 Prompt for AI Agents
In examples/react/search-persistence/src/routes/users.tsx around lines 89-95,
the Reset button currently calls navigate({ search: {} }) which immediately
restores persisted params; fix by clearing the persisted search entry first,
then navigate to an empty search. Implement this by removing the persistence
storage entry (or calling the library's clear/unpersist API) for the search
params and updating any in-memory state, then call navigate to {} (optionally
with replace:true) so the cleared state is actually applied.
middleware: ({ search }: { search: any }) => { | ||
if (!dest.search) { | ||
return {} | ||
} | ||
if (dest.search === true) { | ||
return search | ||
} | ||
return functionalUpdate(dest.search, search) | ||
}, | ||
route: { id: '', fullPath: '' }, | ||
} |
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.
Bug: default middleware returns empty search when dest.search is undefined
When callers don't pass dest.search (the common case), the final middleware returns {}
, discarding the current search. This breaks navigations that should preserve existing search or allow upstream middlewares (e.g., persistence) to operate on it. The default must return the incoming search
unchanged unless an explicit updater or true
is provided.
- const final = {
- middleware: ({ search }: { search: any }) => {
- if (!dest.search) {
- return {}
- }
- if (dest.search === true) {
- return search
- }
- return functionalUpdate(dest.search, search)
- },
+ const final = {
+ middleware: ({ search }: { search: any }) => {
+ // No dest.search provided -> preserve current search
+ if (dest.search === undefined) {
+ return search
+ }
+ // Pass-through
+ if (dest.search === true) {
+ return search
+ }
+ // Apply updater/object
+ return functionalUpdate(dest.search, search)
+ },
route: { id: '', fullPath: '' },
}
This aligns behavior with previous semantics and prevents accidental wiping of search params.
📝 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.
middleware: ({ search }: { search: any }) => { | |
if (!dest.search) { | |
return {} | |
} | |
if (dest.search === true) { | |
return search | |
} | |
return functionalUpdate(dest.search, search) | |
}, | |
route: { id: '', fullPath: '' }, | |
} | |
const final = { | |
middleware: ({ search }: { search: any }) => { | |
// No dest.search provided -> preserve current search | |
if (dest.search === undefined) { | |
return search | |
} | |
// Pass-through | |
if (dest.search === true) { | |
return search | |
} | |
// Apply updater/object | |
return functionalUpdate(dest.search, search) | |
}, | |
route: { id: '', fullPath: '' }, | |
} |
🤖 Prompt for AI Agents
In packages/router-core/src/router.ts around lines 2792 to 2802, the default
middleware currently returns {} when dest.search is undefined which wipes the
incoming search; change the logic so that if dest.search is undefined (or null)
the middleware returns the incoming search unchanged, keep the existing behavior
that if dest.search === true it returns search, and otherwise call
functionalUpdate(dest.search, search); update the conditional checks accordingly
so absence of dest.search preserves the original search.
import { Store } from '@tanstack/store' | ||
import { deepEqual, replaceEqualDeep } from './utils' | ||
import type { NoInfer, PickOptional } from './utils' | ||
import type { SearchMiddleware } from './route' | ||
import type { SearchMiddleware, AnyRoute } from './route' | ||
import type { IsRequiredParams } from './link' | ||
import type { RoutesById, RouteById } from './routeInfo' | ||
import type { RegisteredRouter } from './router' | ||
|
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.
💡 Verification agent
🧩 Analysis chain
Confirm new APIs are re-exported from router-core index.
To ensure consumers can import persistSearchParams
and getSearchPersistenceStore
, verify re-exports exist in packages/router-core/src/index.ts
.
Run this script:
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Check re-exports in packages/router-core/src/index.ts"
rg -n -C2 -g 'packages/router-core/src/index.ts' -P '\bexport\s+\{[^}]*\b(persistSearchParams|getSearchPersistenceStore)\b'
echo
echo "List of exported symbols from index.ts:"
rg -n -C1 -g 'packages/router-core/src/index.ts' -P 'export\s+\{[^}]+\}'
Length of output: 260
Add missing re-exports for search persistence APIs
The functions persistSearchParams
and getSearchPersistenceStore
are implemented in searchMiddleware.ts
but aren’t re-exported from the package entrypoint, so consumers can’t import them.
Please update packages/router-core/src/index.ts
to include:
export { persistSearchParams, getSearchPersistenceStore } from './searchMiddleware'
🧰 Tools
🪛 ESLint
[error] 4-4: Member 'AnyRoute' of the import declaration should be sorted alphabetically.
(sort-imports)
[error] 6-6: Member 'RouteById' of the import declaration should be sorted alphabetically.
(sort-imports)
🤖 Prompt for AI Agents
In packages/router-core/src/searchMiddleware.ts lines 1-8: the functions
persistSearchParams and getSearchPersistenceStore implemented here are not
exported from the package entrypoint; update packages/router-core/src/index.ts
to re-export them by adding an export that forwards persistSearchParams and
getSearchPersistenceStore from './searchMiddleware' so consumers can import
these APIs from the package root.
} | ||
} | ||
|
||
const searchPersistenceStore = new SearchPersistenceStore() |
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.
Global singleton store can leak state across SSR requests.
Creating a module-level singleton introduces cross-request state in Node SSR, potentially leaking one user's search to another. This is a correctness and privacy concern.
Recommend making the store per-router/request or allowing explicit injection to avoid global state on the server. See suggested changes to persistSearchParams
below to accept an optional store and no-op on server unless a store is provided.
🤖 Prompt for AI Agents
In packages/router-core/src/searchMiddleware.ts around line 155, a module-level
singleton SearchPersistenceStore is created which can leak state across SSR
requests; remove the module-level instantiation and refactor consumers to accept
an optional SearchPersistenceStore (injected per-router or per-request). Update
persistSearchParams and any callers to take an optional store parameter, treat
the operation as a no-op on server when no store is provided, and on the client
create a short-lived instance if nothing is injected; also update router
initialization to pass a per-router/request store when needed so no global state
is shared across requests.
export function persistSearchParams<TSearchSchema>( | ||
exclude?: Array<keyof TSearchSchema>, | ||
): SearchMiddleware<TSearchSchema> { | ||
return ({ search, next, route }) => { | ||
// Check if we should restore from store (when search is empty - initial navigation) | ||
const savedSearch = searchPersistenceStore.getSearch(route.id) | ||
let searchToProcess = search |
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.
🛠️ Refactor suggestion
Harden typing and avoid server-side global state by allowing store injection and no-op behavior on SSR.
- Constrain
TSearchSchema
toobject
(consistent with other middleware). - Accept an optional
{ exclude, store }
and avoid using the global store on the server unless one is explicitly provided. - This prevents cross-request leakage while keeping DX nice on the client.
-export function persistSearchParams<TSearchSchema>(
- exclude?: Array<keyof TSearchSchema>,
-): SearchMiddleware<TSearchSchema> {
- return ({ search, next, route }) => {
+export function persistSearchParams<TSearchSchema extends object>(
+ opts?: { exclude?: Array<keyof TSearchSchema>; store?: SearchPersistenceStore },
+): SearchMiddleware<TSearchSchema> {
+ return ({ search, next, route }) => {
+ const isServer = typeof window === 'undefined'
+ // Use injected store if provided. On the server, default to no-op unless a store is supplied.
+ const store = opts?.store ?? (isServer ? undefined : searchPersistenceStore)
+ if (!store) {
+ return next(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.
export function persistSearchParams<TSearchSchema>( | |
exclude?: Array<keyof TSearchSchema>, | |
): SearchMiddleware<TSearchSchema> { | |
return ({ search, next, route }) => { | |
// Check if we should restore from store (when search is empty - initial navigation) | |
const savedSearch = searchPersistenceStore.getSearch(route.id) | |
let searchToProcess = search | |
export function persistSearchParams<TSearchSchema extends object>( | |
opts?: { exclude?: Array<keyof TSearchSchema>; store?: SearchPersistenceStore }, | |
): SearchMiddleware<TSearchSchema> { | |
return ({ search, next, route }) => { | |
const isServer = typeof window === 'undefined' | |
// Use injected store if provided. On the server, default to no-op unless a store is supplied. | |
const store = opts?.store ?? (isServer ? undefined : searchPersistenceStore) | |
if (!store) { | |
return next(search) | |
} | |
// Check if we should restore from store (when search is empty - initial navigation) | |
const savedSearch = searchPersistenceStore.getSearch(route.id) | |
let searchToProcess = search | |
// …rest of the logic… |
const savedSearch = searchPersistenceStore.getSearch(route.id) | ||
let searchToProcess = search | ||
|
||
// If search is empty and we have saved search, restore it | ||
if (savedSearch && Object.keys(savedSearch).length > 0) { | ||
const currentSearch = search as Record<string, unknown> | ||
const shouldRestore = Object.keys(currentSearch).length === 0 || | ||
Object.values(currentSearch).every(value => { | ||
if (value === null || value === undefined || value === '') return true | ||
if (Array.isArray(value) && value.length === 0) return true | ||
if (typeof value === 'object' && value !== null && Object.keys(value).length === 0) return true | ||
return false | ||
}) | ||
|
||
if (shouldRestore) { | ||
searchToProcess = { ...search, ...savedSearch } as TSearchSchema | ||
} | ||
} |
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.
🛠️ Refactor suggestion
Use the injected store; keep restore semantics unchanged.
Wire the retrieval and merge to the selected store. Also prefer consistent naming with opts.exclude
.
- const savedSearch = searchPersistenceStore.getSearch(route.id)
+ const savedSearch = store.getSearch(route.id)
@@
- if (shouldRestore) {
- searchToProcess = { ...search, ...savedSearch } as TSearchSchema
- }
+ if (shouldRestore) {
+ searchToProcess = { ...search, ...savedSearch } as TSearchSchema
+ }
📝 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.
const savedSearch = searchPersistenceStore.getSearch(route.id) | |
let searchToProcess = search | |
// If search is empty and we have saved search, restore it | |
if (savedSearch && Object.keys(savedSearch).length > 0) { | |
const currentSearch = search as Record<string, unknown> | |
const shouldRestore = Object.keys(currentSearch).length === 0 || | |
Object.values(currentSearch).every(value => { | |
if (value === null || value === undefined || value === '') return true | |
if (Array.isArray(value) && value.length === 0) return true | |
if (typeof value === 'object' && value !== null && Object.keys(value).length === 0) return true | |
return false | |
}) | |
if (shouldRestore) { | |
searchToProcess = { ...search, ...savedSearch } as TSearchSchema | |
} | |
} | |
const savedSearch = store.getSearch(route.id) | |
let searchToProcess = search | |
// If search is empty and we have saved search, restore it | |
if (savedSearch && Object.keys(savedSearch).length > 0) { | |
const currentSearch = search as Record<string, unknown> | |
const shouldRestore = Object.keys(currentSearch).length === 0 || | |
Object.values(currentSearch).every(value => { | |
if (value === null || value === undefined || value === '') return true | |
if (Array.isArray(value) && value.length === 0) return true | |
if (typeof value === 'object' && value !== null && Object.keys(value).length === 0) return true | |
return false | |
}) | |
if (shouldRestore) { | |
searchToProcess = { ...search, ...savedSearch } as TSearchSchema | |
} | |
} |
🤖 Prompt for AI Agents
In packages/router-core/src/searchMiddleware.ts around lines 205 to 222, replace
the direct use of searchPersistenceStore with the injected/selected store from
options (e.g., opts.store or the variable you set earlier for the chosen
persistence store) when calling getSearch(route.id), keep the existing
"shouldRestore" checks and the restore merge logic identical (searchToProcess =
{ ...search, ...savedSearch }), and ensure any "exclude" reference uses the
consistent opts.exclude naming (rename local variables to opts.exclude if
needed) so retrieval and merge are performed against the injected store while
preserving current restore semantics.
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.
Actionable comments posted: 1
♻️ Duplicate comments (2)
examples/react/search-persistence/src/routes/users.tsx (1)
124-130
: Reset will immediately restore persisted search unless you clear it firstWith persistence enabled,
{}
triggers restoration. Clear the persisted entry before navigating.Apply this diff:
<button type="button" - onClick={() => navigate({ search: {} } as any)} + onClick={() => { + getSearchPersistenceStore().clearSearch('/users') + navigate({ to: '/users', search: {}, replace: true }) + }} className="border p-2 rounded" > Reset </button>examples/react/search-persistence/src/routes/products.tsx (1)
122-128
: Reset will immediately restore persisted search unless you clear it firstClear the persisted entry before navigating to
{}
.Apply this diff:
<button type="button" - onClick={() => navigate({ search: {} } as any)} + onClick={() => { + getSearchPersistenceStore().clearSearch('/products') + navigate({ to: '/products', search: {}, replace: true }) + }} className="border p-2 rounded" > Reset </button>
🧹 Nitpick comments (11)
examples/react/search-persistence/src/routes/index.tsx (1)
1-1
: Sort named imports to satisfy lint ruleESLint flags sort-imports. Keep members alphabetical to avoid churn in CI.
Apply this diff:
-import { createFileRoute, Link } from '@tanstack/react-router' +import { Link, createFileRoute } from '@tanstack/react-router'examples/react/search-persistence/src/routes/users.tsx (3)
1-5
: Add store import and alphabetize members (lint), preparing for proper ResetYou’ll need the store to clear persisted search before a “reset”. Also fixes the sort-imports rule.
Apply this diff:
-import { - createFileRoute, - useNavigate, - persistSearchParams, -} from '@tanstack/react-router' +import { + createFileRoute, + getSearchPersistenceStore, + persistSearchParams, + useNavigate, +} from '@tanstack/react-router'
9-14
: Coerce numeric search params to play nicely with URL stringsURLs provide values as strings. Using z.coerce.number() prevents unexpected fallback to defaults when querystring contains numeric values.
Apply this diff:
const usersSearchSchema = z.object({ name: z.string().optional().catch(''), status: z.enum(['active', 'inactive', 'all']).optional().catch('all'), - page: z.number().optional().catch(0), - limit: z.number().optional().catch(10), + page: z.coerce.number().optional().catch(0), + limit: z.coerce.number().optional().catch(10), })
91-95
: Removeas any
; provideto
and usereplace
for smoother UXProviding
to
allows proper typing andreplace: true
avoids polluting history while typing.Apply this diff:
- const updateSearch = (updates: Partial<UsersSearchSchema>) => { - navigate({ - search: (prev: UsersSearchSchema) => ({ ...prev, ...updates, page: 0 }), - } as any) - } + const updateSearch = (updates: Partial<UsersSearchSchema>) => { + navigate({ + to: '/users', + replace: true, + search: (prev: UsersSearchSchema) => ({ ...prev, ...updates, page: 0 }), + }) + }docs/router/framework/react/api/router/persistSearchParamsFunction.md (2)
126-127
: Document the Reset pattern to avoid surprise restorationsAdd an explicit example to clear persisted search before navigating to
{}
. This mirrors the example app’s behavior and prevents confusion.Apply this diff to insert a new subsection after the “Automatic Restoration with Links” section:
@@ }+### Resetting persisted search to an empty state
+
+When using persistence, navigating with an empty search{}
will restore saved params. To truly reset, clear the persisted entry first, then navigate.
+
+```tsx
+import { getSearchPersistenceStore, useNavigate } from '@tanstack/react-router'
+
+function ResetButton() {
- const navigate = useNavigate()
- return (
- <button
onClick={() => {
getSearchPersistenceStore().clearSearch('/users')
navigate({ to: '/users', search: {}, replace: true })
}}
Reset
- )
+}
+```@@
Exclusion Strategies
--- `22-27`: **Tighten phrasing for bullet list consistency (optional)** Small clarity tweaks to keep list items parallel and avoid ambiguity about when the middleware runs. Apply this diff: ```diff -**Important**: The middleware only runs when search parameters are being processed. This means: +**Important**: The middleware only runs when search parameters are being processed. For example: @@ -- **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) +- Without a `search` prop: `<Link to="/users">` → Middleware doesn't run → No restoration +- With a `search` function: `<Link to="/users" search={(prev) => prev}>` → Middleware runs → Restoration happens +- With an explicit `search` object: `<Link to="/users" search={{ name: 'John' }}>` → Middleware runs → No restoration (params provided)
examples/react/search-persistence/src/routes/products.tsx (3)
1-5
: Add store import and alphabetize members (lint)Brings in the store for a correct Reset and satisfies sort-imports.
Apply this diff:
-import { - createFileRoute, - useNavigate, - persistSearchParams, -} from '@tanstack/react-router' +import { + createFileRoute, + getSearchPersistenceStore, + persistSearchParams, + useNavigate, +} from '@tanstack/react-router'
9-14
: Coerce numeric search params to handle URL stringsUse z.coerce.number() so URLs like ?minPrice=100 work as intended.
Apply this diff:
const productsSearchSchema = z.object({ category: z.string().optional().catch(''), - minPrice: z.number().optional().catch(0), - maxPrice: z.number().optional().catch(1000), + minPrice: z.coerce.number().optional().catch(0), + maxPrice: z.coerce.number().optional().catch(1000), sortBy: z.enum(['name', 'price', 'rating']).optional().catch('name'), })
66-70
: Removeas any
; provideto
andreplace
for better typing and UXTyping improves and history stays clean while sliding filters.
Apply this diff:
const updateSearch = (updates: Partial<ProductsSearchSchema>) => { navigate({ - search: (prev: ProductsSearchSchema) => ({ ...prev, ...updates }), - } as any) + to: '/products', + replace: true, + search: (prev: ProductsSearchSchema) => ({ ...prev, ...updates }), + }) }examples/react/search-persistence/README.md (2)
63-76
: Add a Reset pattern to avoid surprise restorationsMirror the docs by showing how to clear persisted search before navigating to
{}
.Apply this diff to append after the Manual Restoration snippet:
</Link>+### Resetting persisted search
+
+When using persistence, navigating with an empty search{}
restores the saved state. To truly reset, clear the persisted entry first:
+
+```tsx
+import { getSearchPersistenceStore, useNavigate } from '@tanstack/react-router'
+
+function ResetUsers() {
- const navigate = useNavigate()
- return (
- <button
onClick={() => {
getSearchPersistenceStore().clearSearch('/users')
navigate({ to: '/users', search: {}, replace: true })
}}
Reset Users Search
- )
+}
+```--- `40-41`: **Wording nit: tighten “Important” note** Slight rephrase for clarity and parallelism. Apply this diff: ```diff -⚠️ **Important**: The middleware only runs when search parameters are being processed. Always be explicit about your restoration intent. +⚠️ **Important**: The middleware runs only when search parameters are being processed. Be explicit about your restoration intent.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (12)
docs/router/framework/react/api/router/persistSearchParamsFunction.md
(1 hunks)examples/react/search-persistence/README.md
(1 hunks)examples/react/search-persistence/src/routes/__root.tsx
(1 hunks)examples/react/search-persistence/src/routes/index.tsx
(1 hunks)examples/react/search-persistence/src/routes/products.tsx
(1 hunks)examples/react/search-persistence/src/routes/users.tsx
(1 hunks)examples/react/search-persistence/src/styles.css
(1 hunks)examples/react/search-persistence/src/type-inference-test.ts
(1 hunks)examples/react/search-persistence/src/utils/localStorage-sync.ts
(1 hunks)packages/react-router/src/index.tsx
(1 hunks)packages/router-core/src/router.ts
(3 hunks)packages/router-core/src/searchMiddleware.ts
(3 hunks)
🚧 Files skipped from review as they are similar to previous changes (7)
- packages/react-router/src/index.tsx
- examples/react/search-persistence/src/routes/__root.tsx
- examples/react/search-persistence/src/utils/localStorage-sync.ts
- examples/react/search-persistence/src/styles.css
- packages/router-core/src/router.ts
- packages/router-core/src/searchMiddleware.ts
- examples/react/search-persistence/src/type-inference-test.ts
🧰 Additional context used
🧬 Code Graph Analysis (3)
examples/react/search-persistence/src/routes/index.tsx (3)
examples/react/search-persistence/src/routes/__root.tsx (1)
Route
(4-6)examples/react/search-persistence/src/routes/products.tsx (1)
Route
(18-26)examples/react/search-persistence/src/routes/users.tsx (1)
Route
(18-24)
examples/react/search-persistence/src/routes/products.tsx (2)
examples/react/search-persistence/src/routes/users.tsx (1)
Route
(18-24)packages/router-core/src/searchMiddleware.ts (1)
persistSearchParams
(232-281)
examples/react/search-persistence/src/routes/users.tsx (2)
examples/react/search-persistence/src/routes/products.tsx (1)
Route
(18-26)packages/router-core/src/searchMiddleware.ts (1)
persistSearchParams
(232-281)
🪛 LanguageTool
docs/router/framework/react/api/router/persistSearchParamsFunction.md
[grammar] ~24-~24: There might be a mistake here.
Context: ... Middleware doesn't run → No restoration - With search function: `<Link to="/user...
(QB_NEW_EN)
[grammar] ~25-~25: There might be a mistake here.
Context: ... → Middleware runs → Restoration happens - With explicit search: `<Link to="/user...
(QB_NEW_EN)
examples/react/search-persistence/README.md
[grammar] ~97-~97: There might be a mistake here.
Context: ...ash pnpm install pnpm dev ``` Navigate between Users and Products routes to see automa...
(QB_NEW_EN)
[grammar] ~97-~97: There might be a mistake here.
Context: ... search parameter persistence in action.
(QB_NEW_EN)
🪛 ESLint
examples/react/search-persistence/src/routes/index.tsx
[error] 1-1: Member 'Link' of the import declaration should be sorted alphabetically.
(sort-imports)
examples/react/search-persistence/src/routes/products.tsx
[error] 4-4: Member 'persistSearchParams' of the import declaration should be sorted alphabetically.
(sort-imports)
examples/react/search-persistence/src/routes/users.tsx
[error] 4-4: Member 'persistSearchParams' of the import declaration should be sorted alphabetically.
(sort-imports)
🔇 Additional comments (4)
examples/react/search-persistence/src/routes/index.tsx (1)
7-73
: Nice, concise demo of restoration patternsClear examples showing full restoration, partial override, and clean navigation. This aligns well with the middleware behavior and the docs.
examples/react/search-persistence/src/routes/users.tsx (1)
18-24
: Middleware wiring looks correctRoute-level validateSearch + persistSearchParams() is correctly configured and will persist validated output.
examples/react/search-persistence/src/routes/products.tsx (1)
21-24
: Good use of per-route exclusionExcluding sortBy at the middleware level is a clear, type-safe demo of selective persistence.
examples/react/search-persistence/README.md (1)
97-98
: LGTMContent is clear and actionable; run linters/formatters if your pipeline flags grammar/punctuation.
2. **Restoring**: Restores saved parameters when the middleware is triggered with empty search | ||
|
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:
-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.
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."
hi there! we are right now in a feature freeze until the next alpha version is merged to main. we will revisit this PR after that then. |
Noticed an issue: persisted search params are being saved to routes that weren’t intended. |
i am not sure what you are trying to do here is even possible with search middlewares as they run for links pointing to and from a route, not only when a route is visited |
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.
Actionable comments posted: 6
♻️ Duplicate comments (3)
packages/router-core/src/searchMiddleware.ts (1)
177-177
: Avoid module-scoped singleton: SSR cross-request state leak.Creating
searchPersistenceStore
at module scope leaks state across Node SSR requests. This is a correctness and privacy issue.Apply this minimal fix to make the default store client-only and opt-in on the server:
-const searchPersistenceStore = new SearchPersistenceStore() +const isServer = typeof window === 'undefined' +// Client: a default shared store is OK within a single browser session. +// Server: avoid a module-scoped singleton to prevent cross-request leaks. +let defaultSearchPersistenceStore: SearchPersistenceStore | undefined = + isServer ? undefined : new SearchPersistenceStore()examples/react/search-persistence/src/routes/products.tsx (1)
122-128
: Reset immediately restores persisted search; clear it first (also fix import order)Because persistence restores on empty searches,
navigate({ search: {} })
will reinstate saved params. Clear the persisted entry for/products
before navigating. Also reorder/supplement imports to satisfy lint and add the store accessor.Apply:
import { - createFileRoute, - useNavigate, - persistSearchParams, + createFileRoute, + getSearchPersistenceStore, + persistSearchParams, + useNavigate, } from '@tanstack/react-router'<button type="button" - onClick={() => navigate({ search: {} } as any)} + onClick={() => { + getSearchPersistenceStore().clearSearch('/products') + navigate({ search: {} }) + }} className="border p-2 rounded" > Reset </button>Also applies to: 1-6
examples/react/search-persistence/src/routes/users.tsx (1)
124-130
: Reset immediately restores persisted search; clear it first (also fix import order)As with products, clear the persisted entry for
/users
before navigating to an empty search. Also adjust imports to include the store accessor and satisfy member ordering.Apply:
import { - createFileRoute, - useNavigate, - persistSearchParams, + createFileRoute, + getSearchPersistenceStore, + persistSearchParams, + useNavigate, } from '@tanstack/react-router'<button type="button" - onClick={() => navigate({ search: {} } as any)} + onClick={() => { + getSearchPersistenceStore().clearSearch('/users') + navigate({ search: {} }) + }} className="border p-2 rounded" > Reset </button>Also applies to: 1-6
🧹 Nitpick comments (8)
examples/react/search-persistence/src/main.tsx (2)
20-20
: Place the client-only sync after the router is created (exact insertion point)If you adopt the dynamic import for
setupLocalStorageSync
, place it immediately after the module augmentation (or at least after therouter
is created) to avoid any subtle ordering issues with store initialization.Insert below Line 20:
// Client-only optional localStorage sync for search persistence if (typeof window !== 'undefined') { import('./utils/localStorage-sync') .then((m) => m.setupLocalStorageSync()) .catch(() => {}) }
21-29
: Optional: hydrate when SSR markup exists instead of skipping mount; also guard for non-DOM environmentsIf this example is ever used with SSR, prefer hydrating when the container already has markup rather than skipping render. Also, a small
typeof document !== 'undefined'
guard prevents accidental server import pitfalls.Apply this diff to the mount block:
-const rootElement = document.getElementById('app') -if (rootElement && !rootElement.innerHTML) { - const root = ReactDOM.createRoot(rootElement) - root.render( - <StrictMode> - <RouterProvider router={router} /> - </StrictMode>, - ) -} +const rootElement = + typeof document !== 'undefined' ? document.getElementById('app') : null + +if (!rootElement) { + // SSR/Node: no-op. Client will mount/hydrate. +} else if (rootElement.hasChildNodes()) { + hydrateRoot( + rootElement, + <StrictMode> + <RouterProvider router={router} /> + </StrictMode>, + ) +} else { + createRoot(rootElement).render( + <StrictMode> + <RouterProvider router={router} /> + </StrictMode>, + ) +}And update the import to use the named APIs:
-import ReactDOM from 'react-dom/client' +import { createRoot, hydrateRoot } from 'react-dom/client'packages/router-core/src/searchMiddleware.ts (2)
118-144
: Deduplicate “persistable value” filtering with a private helper for consistency.You repeat the same truthiness/emptiness checks inline. Centralizing this improves readability and keeps semantics consistent if you tweak the rules later (e.g., treating whitespace-only strings as empty).
Apply this focused refactor:
saveSearch< @@ ): void { - const searchRecord = search as Record<string, unknown> - const cleanedSearch = Object.fromEntries( - Object.entries(searchRecord).filter(([_, value]) => { - if (value === null || value === undefined || value === '') return false - if (Array.isArray(value) && value.length === 0) return false - if ( - typeof value === 'object' && - value !== null && - Object.keys(value).length === 0 - ) - return false - return true - }), - ) + const searchRecord = search as Record<string, unknown> + const cleanedSearch = Object.fromEntries( + Object.entries(searchRecord).filter(([_, value]) => + this.#isPersistableValue(value), + ), + ) @@ }) } + + // Treat nullish, empty string, empty array, and empty plain object as "not persistable" + #isPersistableValue(value: unknown) { + if (value === null || value === undefined || value === '') return false + if (Array.isArray(value) && value.length === 0) return false + if (typeof value === 'object' && value !== null) { + // Save non-empty objects only + return Object.keys(value as Record<string, unknown>).length > 0 + } + return true + }
232-235
: Constrain the generic for consistency with other middlewares.Elsewhere you use
TSearchSchema extends object
. Do the same here for consistent inference and to avoid misuse with primitives.-export function persistSearchParams<TSearchSchema>( +export function persistSearchParams<TSearchSchema extends object>(examples/react/search-persistence/src/routes/__root.tsx (2)
42-42
: Gate devtools to development buildsAvoid mounting devtools in production to reduce bundle/CPU overhead and potential SSR mismatches.
Apply:
- <TanStackRouterDevtools /> + {process.env.NODE_ENV === 'development' && <TanStackRouterDevtools />}
21-27
: Consider removing explicit search propagation from top-nav linksUsing
search={(prev) => prev}
in global nav can shuttle unrelated query keys between routes before validation/middleware runs. The middleware already restores route-allowed keys when the target search is empty. Dropping the prop simplifies behavior and avoids transient URL noise. Not critical, just cleaner for the example.Example:
- <Link - to="/users" - search={(prev) => prev} - activeProps={{ - className: 'font-bold', - }} - > + <Link to="/users" activeProps={{ className: 'font-bold' }}> Users </Link>Also applies to: 30-36
examples/react/search-persistence/src/routes/products.tsx (1)
66-70
: Avoidas any
on navigate by typing the updaterYou can satisfy types without
as any
by typing the updater function’s param/return.Example:
- const updateSearch = (updates: Partial<ProductsSearchSchema>) => { - navigate({ - search: (prev: ProductsSearchSchema) => ({ ...prev, ...updates }), - } as any) - } + const updateSearch = (updates: Partial<ProductsSearchSchema>) => { + navigate({ + search: (prev: ProductsSearchSchema): ProductsSearchSchema => ({ + ...prev, + ...updates, + }), + }) + }If your router supports it, alternatively do
const navigate = useNavigate({ from: Route.fullPath })
for stronger typing.examples/react/search-persistence/src/routes/users.tsx (1)
91-95
: Dropas any
on navigateType the updater function instead of casting to
any
to preserve end-to-end search typing.- const updateSearch = (updates: Partial<UsersSearchSchema>) => { - navigate({ - search: (prev: UsersSearchSchema) => ({ ...prev, ...updates, page: 0 }), - } as any) - } + const updateSearch = (updates: Partial<UsersSearchSchema>) => { + navigate({ + search: (prev: UsersSearchSchema): UsersSearchSchema => ({ + ...prev, + ...updates, + page: 0, + }), + }) + }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (6)
examples/react/search-persistence/src/main.tsx
(1 hunks)examples/react/search-persistence/src/routes/__root.tsx
(1 hunks)examples/react/search-persistence/src/routes/index.tsx
(1 hunks)examples/react/search-persistence/src/routes/products.tsx
(1 hunks)examples/react/search-persistence/src/routes/users.tsx
(1 hunks)packages/router-core/src/searchMiddleware.ts
(3 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (6)
examples/react/search-persistence/src/routes/index.tsx (4)
examples/react/search-persistence/src/routes/__root.tsx (1)
Route
(4-6)examples/react/search-persistence/src/routes/users.tsx (1)
Route
(18-24)examples/react/search-persistence/src/routes/products.tsx (1)
Route
(18-26)packages/react-router/src/index.tsx (3)
Route
(260-260)createFileRoute
(142-142)Link
(153-153)
packages/router-core/src/searchMiddleware.ts (4)
packages/router-core/src/index.ts (8)
AnyRoute
(163-163)RegisteredRouter
(215-215)RoutesById
(56-56)RouteById
(53-53)replaceEqualDeep
(277-277)getSearchPersistenceStore
(261-261)persistSearchParams
(260-260)SearchMiddleware
(126-126)packages/router-core/src/route.ts (2)
AnyRoute
(703-718)SearchMiddleware
(80-82)packages/router-core/src/router.ts (1)
RegisteredRouter
(103-103)packages/router-core/src/utils.ts (1)
replaceEqualDeep
(212-259)
examples/react/search-persistence/src/routes/users.tsx (2)
examples/react/search-persistence/src/routes/products.tsx (1)
Route
(18-26)packages/router-core/src/searchMiddleware.ts (1)
persistSearchParams
(232-285)
examples/react/search-persistence/src/routes/__root.tsx (4)
examples/react/search-persistence/src/routes/index.tsx (1)
Route
(3-5)examples/react/search-persistence/src/routes/users.tsx (1)
Route
(18-24)examples/react/search-persistence/src/routes/products.tsx (1)
Route
(18-26)packages/react-router-devtools/src/TanStackRouterDevtools.tsx (1)
TanStackRouterDevtools
(46-126)
examples/react/search-persistence/src/main.tsx (1)
packages/router-core/src/router.ts (1)
Register
(99-101)
examples/react/search-persistence/src/routes/products.tsx (2)
examples/react/search-persistence/src/routes/users.tsx (1)
Route
(18-24)packages/router-core/src/searchMiddleware.ts (1)
persistSearchParams
(232-285)
🪛 ESLint
examples/react/search-persistence/src/routes/index.tsx
[error] 1-1: Member 'Link' of the import declaration should be sorted alphabetically.
(sort-imports)
examples/react/search-persistence/src/routes/users.tsx
[error] 4-4: Member 'persistSearchParams' of the import declaration should be sorted alphabetically.
(sort-imports)
examples/react/search-persistence/src/main.tsx
[error] 5-5: 'setupLocalStorageSync' is defined but never used.
(unused-imports/no-unused-imports)
examples/react/search-persistence/src/routes/products.tsx
[error] 4-4: Member 'persistSearchParams' of the import declaration should be sorted alphabetically.
(sort-imports)
🔇 Additional comments (8)
examples/react/search-persistence/src/main.tsx (1)
13-19
: Default TypeScript include coverssrc/
; no changes neededThe project’s tsconfig.json doesn’t explicitly list an
include
, but by TypeScript’s defaults all.ts
/.tsx
files under the project (includingsrc/
) are included. Your module-augmentation file will therefore be picked up without adding an explicit"include": ["src"]
.packages/router-core/src/searchMiddleware.ts (3)
39-57
: Typing cast for full strip is fine; behavior remains explicit short-circuit.Returning an empty object with a precise
as TSearchSchema
cast wheninput === true
is clear and consistent. No further action needed.
146-159
: Typed getter looks good.
getSearch
returns the per-route typed schema ornull
, which is ergonomic and avoids sentinel empty objects. Nice.
172-174
: Clear-all operation is correct and efficient.Resetting state via an empty object is fine and works well with structural sharing.
examples/react/search-persistence/src/routes/index.tsx (2)
67-81
: Partial override links look goodMerging previous search with an explicit
category
correctly prevents auto-restore and demonstrates the override pattern well.
1-1
: Add a lint script and apply the import‐order fixIt looks like there isn’t a
lint
script defined in your root or examplepackage.json
, so runningpnpm -w lint --fix
(or the npm/yarn equivalents) doesn’t do anything. To resolve the ESLint import-order error:• In your root
package.json
, add:"scripts": { // … "lint": "eslint . --ext .ts,.tsx" }• Then run:
pnpm lint --fix—or invoke ESLint directly without a script—
pnpm exec eslint examples/react/search-persistence/src/routes/index.tsx --ext .ts,.tsx --fix
This will reorder the named imports (
createFileRoute
,Link
) to match your configuredimport/order
rule.examples/react/search-persistence/src/routes/products.tsx (1)
18-26
: Middleware config matches intentPersisting
category/minPrice/maxPrice
while excludingsortBy
aligns with the UI copy and demonstrates selective persistence well.examples/react/search-persistence/src/routes/users.tsx (1)
18-24
: Persisting pagination and filters is sensiblePersisting
name
,status
, andpage
while leavinglimit
transient is a reasonable default for UX.
import { setupLocalStorageSync } from './utils/localStorage-sync' | ||
import './styles.css' | ||
|
||
// Setup localStorage sync for search persistence (optional) | ||
// if (typeof window !== 'undefined') { | ||
// setupLocalStorageSync() | ||
// } |
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.
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.
<div> | ||
<h3 className="text-lg font-medium text-gray-900 mb-4 flex items-center"> | ||
<span className="w-6 h-6 bg-gray-500 rounded-full text-white text-sm flex items-center justify-center mr-3"> | ||
× | ||
</span> | ||
Clean Navigation | ||
</h3> | ||
<p className="text-gray-600 mb-4"> | ||
Navigate without any parameter restoration | ||
</p> | ||
<div className="flex flex-wrap gap-3"> | ||
<Link | ||
to="/users" | ||
className="bg-slate-600 hover:bg-slate-700 text-white px-6 py-3 rounded-lg font-medium transition-colors shadow-md" | ||
> | ||
Users (clean slate) | ||
</Link> | ||
<Link | ||
to="/products" | ||
className="bg-emerald-600 hover:bg-emerald-700 text-white px-6 py-3 rounded-lg font-medium transition-colors shadow-md" | ||
> | ||
Products (clean slate) | ||
</Link> | ||
</div> |
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.
🛠️ Refactor suggestion
“Clean Navigation” currently restores persisted params; clear the store before navigating
With persistence enabled, navigating with an empty search triggers restoration. To truly navigate clean, clear the persisted entry first, then navigate.
Apply:
-import { createFileRoute, Link } from '@tanstack/react-router'
+import {
+ createFileRoute,
+ getSearchPersistenceStore,
+ Link,
+ useNavigate,
+} from '@tanstack/react-router'
function HomeComponent() {
- return (
+ const navigate = useNavigate()
+ return (
- <Link
- to="/users"
- className="bg-slate-600 hover:bg-slate-700 text-white px-6 py-3 rounded-lg font-medium transition-colors shadow-md"
- >
- Users (clean slate)
- </Link>
- <Link
- to="/products"
- className="bg-emerald-600 hover:bg-emerald-700 text-white px-6 py-3 rounded-lg font-medium transition-colors shadow-md"
- >
- Products (clean slate)
- </Link>
+ <button
+ type="button"
+ onClick={() => {
+ getSearchPersistenceStore().clearSearch('/users')
+ navigate({ to: '/users', search: {} })
+ }}
+ className="bg-slate-600 hover:bg-slate-700 text-white px-6 py-3 rounded-lg font-medium transition-colors shadow-md"
+ >
+ Users (clean slate)
+ </button>
+ <button
+ type="button"
+ onClick={() => {
+ getSearchPersistenceStore().clearSearch('/products')
+ navigate({ to: '/products', search: {} })
+ }}
+ className="bg-emerald-600 hover:bg-emerald-700 text-white px-6 py-3 rounded-lg font-medium transition-colors shadow-md"
+ >
+ Products (clean slate)
+ </button>
Also applies to: 1-1, 7-8
🤖 Prompt for AI Agents
In examples/react/search-persistence/src/routes/index.tsx around lines 84 to
107, the "Clean Navigation" links still restore persisted params because the
persistence store isn't cleared before navigation; update the Link handlers to
clear the persisted state entry (e.g., call the store clear/remove API for the
relevant key) immediately before performing navigation so that navigating to
/users or /products uses a clean slate; apply the same change to the other
instances noted (lines 1-1 and 7-8) so all "clean" navigation paths clear the
persisted params first.
export function getSearchPersistenceStore< | ||
TRouteTree extends AnyRoute = RegisteredRouter['routeTree'], | ||
>(): { | ||
state: { | ||
[K in keyof RoutesById<TRouteTree>]: RouteById< | ||
TRouteTree, | ||
K | ||
>['types']['fullSearchSchema'] | ||
} | ||
store: Store<{ | ||
[K in keyof RoutesById<TRouteTree>]: RouteById< | ||
TRouteTree, | ||
K | ||
>['types']['fullSearchSchema'] | ||
}> | ||
subscribe: (listener: () => void) => () => void | ||
getSearch: <TRouteId extends keyof RoutesById<TRouteTree>>( | ||
routeId: TRouteId, | ||
) => RouteById<TRouteTree, TRouteId>['types']['fullSearchSchema'] | null | ||
saveSearch: <TRouteId extends keyof RoutesById<TRouteTree>>( | ||
routeId: TRouteId, | ||
search: RouteById<TRouteTree, TRouteId>['types']['fullSearchSchema'], | ||
) => void | ||
clearSearch: <TRouteId extends keyof RoutesById<TRouteTree>>( | ||
routeId: TRouteId, | ||
) => void | ||
clearAllSearches: () => void | ||
} { | ||
return { | ||
get state() { | ||
return searchPersistenceStore.getTypedState<TRouteTree>() | ||
}, | ||
get store() { | ||
return searchPersistenceStore.getTypedStore<TRouteTree>() | ||
}, | ||
subscribe: (listener: () => void) => | ||
searchPersistenceStore.subscribe(listener), | ||
getSearch: <TRouteId extends keyof RoutesById<TRouteTree>>( | ||
routeId: TRouteId, | ||
) => searchPersistenceStore.getSearch<TRouteTree, TRouteId>(routeId), | ||
saveSearch: <TRouteId extends keyof RoutesById<TRouteTree>>( | ||
routeId: TRouteId, | ||
search: RouteById<TRouteTree, TRouteId>['types']['fullSearchSchema'], | ||
) => | ||
searchPersistenceStore.saveSearch<TRouteTree, TRouteId>(routeId, search), | ||
clearSearch: <TRouteId extends keyof RoutesById<TRouteTree>>( | ||
routeId: TRouteId, | ||
) => searchPersistenceStore.clearSearch<TRouteTree, TRouteId>(routeId), | ||
clearAllSearches: () => searchPersistenceStore.clearAllSearches(), | ||
} | ||
} |
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.
🛠️ Refactor suggestion
Make getSearchPersistenceStore injectable and safe for SSR; avoid relying on a global on the server.
To fully resolve SSR leakage and enable per-request stores, accept an optional store and fall back to the client default only in the browser. For server calls without injection, return an ephemeral store instance so usage remains non-throwing.
-export function getSearchPersistenceStore<
- TRouteTree extends AnyRoute = RegisteredRouter['routeTree'],
->(): {
+export function getSearchPersistenceStore<
+ TRouteTree extends AnyRoute = RegisteredRouter['routeTree'],
+>(store?: SearchPersistenceStore): {
@@
-} {
- return {
- get state() {
- return searchPersistenceStore.getTypedState<TRouteTree>()
- },
- get store() {
- return searchPersistenceStore.getTypedStore<TRouteTree>()
- },
- subscribe: (listener: () => void) =>
- searchPersistenceStore.subscribe(listener),
- getSearch: <TRouteId extends keyof RoutesById<TRouteTree>>(
- routeId: TRouteId,
- ) => searchPersistenceStore.getSearch<TRouteTree, TRouteId>(routeId),
- saveSearch: <TRouteId extends keyof RoutesById<TRouteTree>>(
- routeId: TRouteId,
- search: RouteById<TRouteTree, TRouteId>['types']['fullSearchSchema'],
- ) =>
- searchPersistenceStore.saveSearch<TRouteTree, TRouteId>(routeId, search),
- clearSearch: <TRouteId extends keyof RoutesById<TRouteTree>>(
- routeId: TRouteId,
- ) => searchPersistenceStore.clearSearch<TRouteTree, TRouteId>(routeId),
- clearAllSearches: () => searchPersistenceStore.clearAllSearches(),
- }
-}
+} {
+ const selected =
+ store ?? defaultSearchPersistenceStore ?? new SearchPersistenceStore()
+
+ return {
+ get state() {
+ return selected.getTypedState<TRouteTree>()
+ },
+ get store() {
+ return selected.getTypedStore<TRouteTree>()
+ },
+ subscribe: (listener: () => void) => selected.subscribe(listener),
+ getSearch: <TRouteId extends keyof RoutesById<TRouteTree>>(routeId: TRouteId) =>
+ selected.getSearch<TRouteTree, TRouteId>(routeId),
+ saveSearch: <TRouteId extends keyof RoutesById<TRouteTree>>(
+ routeId: TRouteId,
+ search: RouteById<TRouteTree, TRouteId>['types']['fullSearchSchema'],
+ ) => selected.saveSearch<TRouteTree, TRouteId>(routeId, search),
+ clearSearch: <TRouteId extends keyof RoutesById<TRouteTree>>(routeId: TRouteId) =>
+ selected.clearSearch<TRouteTree, TRouteId>(routeId),
+ clearAllSearches: () => selected.clearAllSearches(),
+ }
+}
Notes:
- On the server, when no store is injected, the function now yields an ephemeral instance scoped to the call (no cross-request sharing).
- Consumers that want durable SSR persistence (e.g., DB-backed) can construct a
SearchPersistenceStore
per request and pass it in.
Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
packages/router-core/src/searchMiddleware.ts lines 180-230: make
getSearchPersistenceStore accept an optional SearchPersistenceStore parameter
and avoid using a global on the server; if a store is provided, use it,
otherwise if running in the browser fall back to the existing client default
store, and if running on the server create and return a new ephemeral per-call
store instance (scoped to the request) so SSR doesn't leak state across
requests; preserve all existing generic typings and exported API (state, store,
subscribe, getSearch, saveSearch, clearSearch, clearAllSearches) and ensure the
ephemeral store implements the same interface and is non-throwing when used.
export function persistSearchParams<TSearchSchema>( | ||
persistedSearchParams: Array<keyof TSearchSchema>, | ||
exclude?: Array<keyof TSearchSchema>, | ||
): SearchMiddleware<TSearchSchema> { | ||
return ({ search, next, route }) => { | ||
// Filter input to only explicitly allowed keys for this route | ||
const searchRecord = search as Record<string, unknown> | ||
const allowedKeysStr = persistedSearchParams.map((key) => String(key)) | ||
const filteredSearch = Object.fromEntries( | ||
Object.entries(searchRecord).filter(([key]) => | ||
allowedKeysStr.includes(key), | ||
), | ||
) as TSearchSchema | ||
|
||
// Restore from store if current search is empty | ||
const savedSearch = searchPersistenceStore.getSearch(route.id) | ||
let searchToProcess = filteredSearch | ||
|
||
if (savedSearch && Object.keys(savedSearch).length > 0) { | ||
const currentSearch = filteredSearch as Record<string, unknown> | ||
const isEmpty = Object.keys(currentSearch).length === 0 | ||
|
||
if (isEmpty) { | ||
searchToProcess = savedSearch as TSearchSchema | ||
} | ||
} | ||
|
||
const result = next(searchToProcess) | ||
|
||
// Save only the allowed parameters for persistence | ||
const resultRecord = result as Record<string, unknown> | ||
if (Object.keys(resultRecord).length > 0) { | ||
const persistedKeysStr = persistedSearchParams.map((key) => String(key)) | ||
const paramsToSave = Object.fromEntries( | ||
Object.entries(resultRecord).filter(([key]) => | ||
persistedKeysStr.includes(key), | ||
), | ||
) | ||
|
||
const excludeKeys = exclude ? exclude.map((key) => String(key)) : [] | ||
const filteredResult = Object.fromEntries( | ||
Object.entries(paramsToSave).filter( | ||
([key]) => !excludeKeys.includes(key), | ||
), | ||
) | ||
|
||
if (Object.keys(filteredResult).length > 0) { | ||
searchPersistenceStore.saveSearch(route.id, filteredResult) | ||
} | ||
} | ||
|
||
return result | ||
} | ||
} |
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.
💡 Verification agent
🧩 Analysis chain
Add unit tests for persistence/restore semantics (empty vs. missing) and clearing.
Given the subtlety here, add tests that assert:
- Saving happens only for allowed keys (and excludes are respected).
- Restoring occurs when incoming search is empty, and optionally when keys are missing (if you adopt
restoreOn
). - Clearing: when no allowed keys are returned by
next
, the persisted entry is removed.
You can scaffold the tests with these search patterns to find integration points:
🏁 Script executed:
#!/bin/bash
# Identify middleware usages and examples to target for tests
rg -n -C3 --type=ts '\bpersistSearchParams\s*\('
rg -n -C3 --type=ts '\bgetSearchPersistenceStore\s*\('
# Check if any server/SSR paths import these (to cover injection/no-op behavior)
rg -n -C3 --type=ts -g '!**/node_modules/**' '(server|ssr|entry-server|api|routes/.*loader)\.tsx?'
Length of output: 10561
🏁 Script executed:
#!/bin/bash
# Search for existing test definitions in packages/router-core
rg -n -C3 --type=ts '(describe\(|test\()' -g 'packages/router-core/**'
Length of output: 39210
Add missing unit tests for persistSearchParams
middleware
There are currently no tests covering the persistence and restoration behavior introduced in packages/router-core/src/searchMiddleware.ts
. Please add a new test suite (e.g. packages/router-core/tests/searchMiddleware.test.ts
) that verifies the following scenarios:
- Saving only allowed keys and respecting the
exclude
list - Restoring from the store when the incoming search object is empty
- Optionally restoring when specific keys are missing (if
restoreOn
behavior is adopted) - Clearing the persisted entry when the middleware’s
next
result has no allowed keys
Suggested test locations and patterns (use vitest
):
• Target the persistSearchParams([...], [...])
invocation in your tests, mocking searchPersistenceStore.getSearch
and saveSearch
.
• Use example routes in examples/react/search-persistence/src/routes/{users,products}.tsx
as integration points.
• Scaffold with these rg
patterns to confirm coverage targets:
rg -n "'persistSearchParams'" -g 'packages/router-core/**'
rg -n "'searchPersistenceStore'" -g 'packages/router-core/**'
🤖 Prompt for AI Agents
In packages/router-core/src/searchMiddleware.ts around lines 232-285, there are
no unit tests for persistSearchParams; add a new vitest test suite at
packages/router-core/tests/searchMiddleware.test.ts that imports
persistSearchParams, mocks searchPersistenceStore.getSearch and saveSearch, and
verifies: (1) only allowed keys are saved and the exclude list is respected, (2)
when incoming search is empty the middleware restores saved values, (3) behavior
when specific keys are missing if you choose to add/verify restore-on-key logic,
and (4) persisted entry is cleared/not saved when next result contains no
allowed keys; implement tests by creating simple route stubs, calling the
middleware with controlled search/next results, asserting calls and payloads to
mocked getSearch/saveSearch, and ensure cleanup/reset of mocks between tests.
🛠️ Refactor suggestion
Injectable, typed middleware + fixed clearing semantics.
Two issues to address:
- Current signature lacks
extends object
, and there’s no way to inject a per-request store. Add an options object withstore
andexclude
. - Stale persistence bug: when the route returns no allowed keys, the middleware never clears prior saved values. Call
saveSearch
even for an empty filtered result soSearchPersistenceStore
can delete the route entry.
This also adds an optional restoreOn
switch to control restoration semantics.
-export function persistSearchParams<TSearchSchema>(
- persistedSearchParams: Array<keyof TSearchSchema>,
- exclude?: Array<keyof TSearchSchema>,
-): SearchMiddleware<TSearchSchema> {
- return ({ search, next, route }) => {
+export function persistSearchParams<TSearchSchema extends object>(
+ persistedSearchParams: Array<keyof TSearchSchema>,
+ opts?: {
+ exclude?: Array<keyof TSearchSchema>
+ store?: SearchPersistenceStore
+ // empty: restore only when no allowed keys are present in the incoming search
+ // missing: fill in missing allowed keys from persistence (can be more "sticky")
+ // never: do not restore automatically
+ restoreOn?: 'empty' | 'missing' | 'never'
+ },
+): SearchMiddleware<TSearchSchema> {
+ return ({ search, next, route }) => {
+ const isServer = typeof window === 'undefined'
+ const storeRef =
+ opts?.store ?? (isServer ? undefined : defaultSearchPersistenceStore)
@@
- const allowedKeysStr = persistedSearchParams.map((key) => String(key))
+ const allowedKeysStr = persistedSearchParams.map((key) => String(key))
const filteredSearch = Object.fromEntries(
Object.entries(searchRecord).filter(([key]) =>
allowedKeysStr.includes(key),
),
) as TSearchSchema
@@
- const savedSearch = searchPersistenceStore.getSearch(route.id)
+ const savedSearch = storeRef?.getSearch(route.id)
let searchToProcess = filteredSearch
-
- if (savedSearch && Object.keys(savedSearch).length > 0) {
- const currentSearch = filteredSearch as Record<string, unknown>
- const isEmpty = Object.keys(currentSearch).length === 0
-
- if (isEmpty) {
- searchToProcess = savedSearch as TSearchSchema
- }
- }
+ const restoreOn = opts?.restoreOn ?? 'empty'
+ if (savedSearch && restoreOn !== 'never') {
+ const current = filteredSearch as Record<string, unknown>
+ const isEmpty = Object.keys(current).length === 0
+ if (restoreOn === 'empty' && isEmpty) {
+ searchToProcess = savedSearch as TSearchSchema
+ } else if (restoreOn === 'missing') {
+ // Merge persisted keys for any missing allowed keys
+ searchToProcess = { ...(savedSearch as object), ...filteredSearch } as TSearchSchema
+ }
+ }
@@
- // Save only the allowed parameters for persistence
- const resultRecord = result as Record<string, unknown>
- if (Object.keys(resultRecord).length > 0) {
- const persistedKeysStr = persistedSearchParams.map((key) => String(key))
- const paramsToSave = Object.fromEntries(
- Object.entries(resultRecord).filter(([key]) =>
- persistedKeysStr.includes(key),
- ),
- )
-
- const excludeKeys = exclude ? exclude.map((key) => String(key)) : []
- const filteredResult = Object.fromEntries(
- Object.entries(paramsToSave).filter(
- ([key]) => !excludeKeys.includes(key),
- ),
- )
-
- if (Object.keys(filteredResult).length > 0) {
- searchPersistenceStore.saveSearch(route.id, filteredResult)
- }
- }
+ // Save only the allowed parameters (after excludes). Always call save so empty results clear prior state.
+ const resultRecord = result as Record<string, unknown>
+ const persistedKeysStr = persistedSearchParams.map((key) => String(key))
+ const paramsToSave = Object.fromEntries(
+ Object.entries(resultRecord).filter(([key]) =>
+ persistedKeysStr.includes(key),
+ ),
+ )
+ const excludeKeys = opts?.exclude ? opts.exclude.map((k) => String(k)) : []
+ const filteredResult = Object.fromEntries(
+ Object.entries(paramsToSave).filter(([key]) => !excludeKeys.includes(key)),
+ )
+ if (storeRef) {
+ storeRef.saveSearch(route.id, filteredResult)
+ }
@@
return result
}
}
Rationale:
extends object
aligns with other middlewares.- Options-based API enables per-request store injection and typed excludes without broadening the positional API.
- Clearing bug resolved by saving even when empty, leveraging
SearchPersistenceStore.saveSearch
’s deletion branch. restoreOn
lets consumers choose between minimal (“empty”) and sticky (“missing”) restoration.
Do you want me to propagate the new optional store
parameter through the React integration and example so SSR usage is explicit and leak-free?
Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In packages/router-core/src/searchMiddleware.ts around lines 232-285, the
middleware signature and behavior need updating: change the generic to
TSearchSchema extends object and replace the positional
persistedSearchParams/exclude parameters with a single options object {
persistedSearchParams: Array<keyof TSearchSchema>, exclude?: Array<keyof
TSearchSchema>, store?: SearchPersistenceStore, restoreOn?: 'empty' | 'missing'
} so callers can inject a per-request store and typed excludes; use
options.store (falling back to the module-level store) for getSearch/saveSearch
calls; implement restoreOn so when restoreOn === 'empty' you only restore
savedSearch when current filtered search has zero keys, and when restoreOn ===
'missing' you restore when any allowed key is absent; when persisting, always
call store.saveSearch(route.id, filteredResult) even if filteredResult is empty
so the store can clear prior entries; keep return value as next(resultSearch)
and preserve existing filtering logic but use the new options and typed
constraints.
// Save only the allowed parameters for persistence | ||
const resultRecord = result as Record<string, unknown> | ||
if (Object.keys(resultRecord).length > 0) { | ||
const persistedKeysStr = persistedSearchParams.map((key) => String(key)) | ||
const paramsToSave = Object.fromEntries( | ||
Object.entries(resultRecord).filter(([key]) => | ||
persistedKeysStr.includes(key), | ||
), | ||
) | ||
|
||
const excludeKeys = exclude ? exclude.map((key) => String(key)) : [] | ||
const filteredResult = Object.fromEntries( | ||
Object.entries(paramsToSave).filter( | ||
([key]) => !excludeKeys.includes(key), | ||
), | ||
) | ||
|
||
if (Object.keys(filteredResult).length > 0) { | ||
searchPersistenceStore.saveSearch(route.id, filteredResult) | ||
} | ||
} |
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.
Bug: persisted values aren’t cleared when results are empty.
As written, if the route returns no allowed keys, prior values remain persisted, causing stale search state on subsequent navigations.
If you prefer a minimal change (without the broader refactor above), apply this:
- // Save only the allowed parameters for persistence
- const resultRecord = result as Record<string, unknown>
- if (Object.keys(resultRecord).length > 0) {
+ // Save only the allowed parameters for persistence
+ const resultRecord = result as Record<string, unknown>
const persistedKeysStr = persistedSearchParams.map((key) => String(key))
const paramsToSave = Object.fromEntries(
Object.entries(resultRecord).filter(([key]) =>
persistedKeysStr.includes(key),
),
)
- const excludeKeys = exclude ? exclude.map((key) => String(key)) : []
+ const excludeKeys = exclude ? exclude.map((key) => String(key)) : []
const filteredResult = Object.fromEntries(
Object.entries(paramsToSave).filter(
([key]) => !excludeKeys.includes(key),
),
)
- if (Object.keys(filteredResult).length > 0) {
- searchPersistenceStore.saveSearch(route.id, filteredResult)
- }
- }
+ // Always call save; an empty object clears the entry.
+ searchPersistenceStore.saveSearch(route.id, filteredResult)
📝 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.
// Save only the allowed parameters for persistence | |
const resultRecord = result as Record<string, unknown> | |
if (Object.keys(resultRecord).length > 0) { | |
const persistedKeysStr = persistedSearchParams.map((key) => String(key)) | |
const paramsToSave = Object.fromEntries( | |
Object.entries(resultRecord).filter(([key]) => | |
persistedKeysStr.includes(key), | |
), | |
) | |
const excludeKeys = exclude ? exclude.map((key) => String(key)) : [] | |
const filteredResult = Object.fromEntries( | |
Object.entries(paramsToSave).filter( | |
([key]) => !excludeKeys.includes(key), | |
), | |
) | |
if (Object.keys(filteredResult).length > 0) { | |
searchPersistenceStore.saveSearch(route.id, filteredResult) | |
} | |
} | |
// Save only the allowed parameters for persistence | |
const resultRecord = result as Record<string, unknown> | |
const persistedKeysStr = persistedSearchParams.map((key) => String(key)) | |
const paramsToSave = Object.fromEntries( | |
Object.entries(resultRecord).filter(([key]) => | |
persistedKeysStr.includes(key), | |
), | |
) | |
const excludeKeys = exclude ? exclude.map((key) => String(key)) : [] | |
const filteredResult = Object.fromEntries( | |
Object.entries(paramsToSave).filter( | |
([key]) => !excludeKeys.includes(key), | |
), | |
) | |
// Always call save; an empty object clears the entry. | |
searchPersistenceStore.saveSearch(route.id, filteredResult) |
🤖 Prompt for AI Agents
In packages/router-core/src/searchMiddleware.ts around lines 261 to 281, the
current logic only saves filteredResult when it has keys, leaving any previously
persisted values intact when filteredResult is empty; update the code so that
after computing filteredResult you either call
searchPersistenceStore.saveSearch(route.id, filteredResult) when it has keys or
explicitly clear the persisted entry when it is empty (e.g., call
searchPersistenceStore.saveSearch(route.id, {}) or the store's delete/clear
method) so stale values are removed.
✨ Summary
This PR adds a new middleware and supporting API for search parameter persistence:
persistSearchParams
middlewareAutomatically saves and restores search params across navigations, with support for selective exclusion. Works seamlessly with SSR and remains framework-agnostic, with a React integration provided.
getSearchPersistenceStore()
APIExposes a fully typed store for manual save, restore, and clear operations. Gives developers full control when they need to override the default middleware behavior.
Examples & Documentation
Includes a new
search-persistence
React example (Users/Products routes) demonstrating how persistence works in practice. The example also shows how the store can be synchronized withlocalStorage
for durability across reloads, while making it clear that other adapters (e.g. saving to a database in SSR environments) can be easily built on top of the same API.🚀 Why This Is Useful
getSearchPersistenceStore()
abstraction makes it trivial to integrate persistence into any storage layer:localStorage
, session storage)📚 Additions
persistSearchParams
middleware in@tanstack/router-core
getSearchPersistenceStore()
API with type inferenceexamples/react/search-persistence
Summary by CodeRabbit
New Features
Documentation
Chores