Skip to content

Conversation

NitsanCohen770
Copy link

@NitsanCohen770 NitsanCohen770 commented Aug 20, 2025

✨ Summary

This PR adds a new middleware and supporting API for search parameter persistence:

  • persistSearchParams middleware
    Automatically 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() API
    Exposes 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 with localStorage 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

  • Improved UX – Users no longer lose their filters, pagination, or search state when navigating away and back.
  • Flexibility – Developers can choose which parameters persist, or clear them entirely, depending on the use case.
  • Extensible – The getSearchPersistenceStore() abstraction makes it trivial to integrate persistence into any storage layer:
    • Client-side (e.g. localStorage, session storage)
    • Server-side (e.g. saving to a database or session store during SSR)
  • Typed by default – All persistence APIs automatically infer route-specific search schemas for safety and DX.

📚 Additions

  • persistSearchParams middleware in @tanstack/router-core
  • getSearchPersistenceStore() API with type inference
  • New React example: examples/react/search-persistence
  • Documentation for usage patterns, caveats, and extension points

Summary by CodeRabbit

  • New Features

    • Search-parameter persistence: key-based middleware, typed per-route persistence store, router option to enable persistence, and APIs to save/restore/clear persisted searches.
  • Documentation

    • New docs detailing middleware usage, props, exclusion/restore patterns, SSR guidance, manual store usage, and examples.
  • Chores

    • New React examples (client and SSR) demonstrating persistence, localStorage sync, build/config files, and a type-inference test.

- 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
Copy link

coderabbitai bot commented Aug 20, 2025

Walkthrough

Adds typed, route-aware search-parameter persistence: a SearchPersistenceStore class, create/get accessors, persistSearchParams middleware, router plumbing to pass route + router into search middleware, barrel re-exports, docs, and complete React + SSR examples (localStorage and server DB sync) demonstrating usage, exclusions, and restoration. (50 words)

Changes

Cohort / File(s) Summary
Docs
docs/router/framework/react/api/router/persistSearchParamsFunction.md
New documentation for persistSearchParams, store APIs, trigger semantics, exclusion strategies, restoration examples, and SSR guidance.
Examples: client search-persistence app
examples/react/search-persistence/index.html, examples/react/search-persistence/package.json, examples/react/search-persistence/postcss.config.mjs, examples/react/search-persistence/tailwind.config.mjs, examples/react/search-persistence/vite.config.js, examples/react/search-persistence/tsconfig.json, examples/react/search-persistence/src/styles.css
Adds Vite + React + Tailwind project scaffolding and build/dev configs.
Examples: client app code & routes
examples/react/search-persistence/src/main.tsx, .../src/routeTree.gen.ts, .../src/routes/__root.tsx, .../src/routes/index.tsx, .../src/routes/users.tsx, .../src/routes/products.tsx, .../src/routes/users.$userId.tsx
App bootstrap, route-tree generation, and route modules demonstrating persistSearchParams, retain/stripSearchParams, zod schemas, UIs, and restoration patterns.
Examples: client utils & tests
examples/react/search-persistence/src/utils/localStorage-sync.ts, examples/react/search-persistence/src/type-inference-test.ts, examples/react/search-persistence/README.md
localStorage sync helper for persistence store, type-inference test exercising typed APIs, and README for the example.
Examples: SSR search-persistence
examples/react/ssr-search-persistence/**
Full SSR example: server, client/server entries, fetch polyfill, per-request store wiring, server DB demo, routes, generated routeTree, Vite SSR config, styles, and README.
Core: public re-exports (barrels)
packages/router-core/src/index.ts, packages/react-router/src/index.tsx
Re-exports added: SearchPersistenceStore, createSearchPersistenceStore, persistSearchParams, and getSearchPersistenceStore.
Core: middleware + types
packages/router-core/src/route.ts, packages/router-core/src/router.ts
Adds route-aware search middleware typings/context (route, router with optional searchPersistenceStore), RouterOptions.searchPersistenceStore, and refactors middleware pipeline to carry per-route metadata.
Core: search persistence implementation
packages/router-core/src/searchMiddleware.ts
Implements SearchPersistenceStore (store/get/save/clear APIs), createSearchPersistenceStore, typed getSearchPersistenceStore accessor, and persistSearchParams middleware with restore/persist/exclusion logic; updates retain/stripSearchParams typings/behavior.
Misc / tooling
port-tanstack-router-e2e-react-basic*.txt
Adds two port files with numeric contents for e2e config.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant UI as Route Component
  participant Router as Router
  participant MW as persistSearchParams
  participant Store as SearchPersistenceStore
  Note over Router,MW: Middleware invoked with route metadata {id, fullPath} and router

  UI->>Router: navigate(to, search?)
  Router->>MW: invoke({ search, next, route, router })
  alt incoming search is empty
    MW->>Store: getSearch(route.id)
    Store-->>MW: savedSearch|null
    MW->>MW: if savedSearch -> restore/merge
  end
  MW->>Router: next(merged/filteredSearch)
  alt resulting search non-empty
    MW->>Store: saveSearch(route.id, filteredSearch)
  end
  Router-->>UI: render with finalSearch
Loading
sequenceDiagram
  autonumber
  participant App as App bootstrap
  participant LS as localStorage
  participant Store as SearchPersistenceStore

  App->>LS: getItem('search-persistence')
  LS-->>App: serialized state
  App->>Store: saveSearch(routeId, search) per entry (rehydrate)
  Store-->>App: notify subscribers
  App->>LS: setItem('search-persistence', serialized state) on updates
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

  • Search Params as Actual State #4973 — This PR implements the requested persistence primitives: SearchPersistenceStore, persistSearchParams, getSearchPersistenceStore, and router wiring for per-router stores.

Possibly related PRs

Suggested reviewers

  • schiller-manuel
  • SeanCassiere

Poem

I hop through routes with gentle care,
Tucking queries in my fuzzy lair.
A tiny store keeps keys snug and warm,
Restored on paths when storms transform.
I nibble an exclude, then bound—search stays safe and sound! 🐇✨

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 Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

- 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
Copy link

@coderabbitai coderabbitai bot left a 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 heading

Minor 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 exists

Using 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 search

You 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 readability

Add “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: Strengthen SearchMiddlewareContext.route typing

The router already threads the route object (with id and fullPath) through the middleware pipeline in router.ts, so no changes are needed there. To enforce immutability and reuse the existing RouteExtensions 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 example

Minor 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 scope

Styling 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 listener

In 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 state

Without 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-pressure

Search 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 purpose

The 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)

12-12: Use semantic

for the primary navigation

Small 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 lean

Gate 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 specifiers

Complies 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 rule

ESLint 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 URL

Query-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: Avoid as any in navigate; keep types intact

Let 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 change

Avoid 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 restoration

Improve 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 persistence

Since {} 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 cleanliness

Alphabetize 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 URL

Use 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: Avoid as any in navigate; keep types intact

Prefer 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 change

Avoid 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 search

Search 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 in clearSearch; 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.

📥 Commits

Reviewing files that changed from the base of the PR and between 3f05c0b and 30f6404.

📒 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 example

Strict mode, DOM libs, and esModuleInterop look good here.

examples/react/search-persistence/tailwind.config.mjs (1)

1-4: LGTM

Minimal Tailwind config with correct content globs for Vite projects.

examples/react/search-persistence/postcss.config.mjs (1)

1-6: LGTM

PostCSS + Tailwind + Autoprefixer default export is correct for ESM config.

examples/react/search-persistence/vite.config.js (2)

1-14: Plugin order and options look correct

Router plugin first, then React plugin, with code-splitting enabled. Good.


1-14: Router and React Vite plugins are declared

The example’s package.json already includes the required plugins:

  • examples/react/search-persistence/package.json
    "@tanstack/router-plugin" in dependencies
    "@vitejs/plugin-react" in devDependencies

No changes needed.

examples/react/search-persistence/README.md (1)

1-15: Clear, action-oriented README

Nice 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 APIs

Confirmed 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 that packages/router-core/src/index.ts does not re-export any of these identifiers more than once, and packages/router-core/src/searchMiddleware.ts defines and exports retainSearchParams, stripSearchParams, persistSearchParams, and getSearchPersistenceStore. No further action required.

examples/react/search-persistence/src/main.tsx (2)

15-19: Type registration for the router is correctly declared

The module augmentation pattern for Register looks correct and should fully type getSearchPersistenceStore for this app.


21-29: If you later SSR this example, consider hydrateRoot

The 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-router

I 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 standalone

Monorepo 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.search

Using 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 fine

The 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-structured

Collecting 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 solid

Nice 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 good

The guidance to always specify restoration intent will save users from surprises later. Looks great.


166-209: Store API examples are clear and useful

Good coverage of get/save/clear patterns and integrating with useStore.

examples/react/search-persistence/src/routes/users.tsx (2)

43-57: useMemo dependencies are sufficient

Filtering logic only depends on name and status; memoization scope is correct.


14-22: Middleware usage matches core implementation

persistSearchParams() 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 correct

Cloning before sorting prevents mutating the source; predicates look good.


14-22: Excluding sortBy from persistence is a good default

Keeps 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.

Comment on lines +114 to +120
<button
type="button"
onClick={() => navigate({ search: {} } as any)}
className="border p-2 rounded"
>
Reset
</button>
Copy link

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.

Suggested change
<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.

Comment on lines +89 to +95
<button
type="button"
onClick={() => navigate({ search: {} } as any)}
className="border p-2 rounded"
>
Reset
</button>
Copy link

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.

Suggested change
<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.

Comment on lines 1 to 8
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'

Copy link

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.

Copy link

@coderabbitai coderabbitai bot left a 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 first

With 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 first

Clear 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 rule

ESLint 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 Reset

You’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 strings

URLs 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: Remove as any; provide to and use replace for smoother UX

Providing to allows proper typing and replace: 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 restorations

Add 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 strings

Use 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: Remove as any; provide to and replace for better typing and UX

Typing 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 restorations

Mirror 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.

📥 Commits

Reviewing files that changed from the base of the PR and between 30f6404 and 41d598f.

📒 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 patterns

Clear 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 correct

Route-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 exclusion

Excluding sortBy at the middleware level is a clear, type-safe demo of selective persistence.

examples/react/search-persistence/README.md (1)

97-98: LGTM

Content is clear and actionable; run linters/formatters if your pipeline flags grammar/punctuation.

Comment on lines +20 to +21
2. **Restoring**: Restores saved parameters when the middleware is triggered with empty search

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Grammar nit: add article for clarity

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

Apply this diff:

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

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

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

@schiller-manuel
Copy link
Contributor

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.

@NitsanCohen770
Copy link
Author

Noticed an issue: persisted search params are being saved to routes that weren’t intended.
Cause: the middleware currently runs on both the from and to routes.
Working on a fix ASAP.

@schiller-manuel
Copy link
Contributor

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

@NitsanCohen770
Copy link
Author

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

Yes, this is exactly what I noticed.

I’ve found a solution by making the search params you want to persist an explicit parameter:

export function persistSearchParams<TSearchSchema>(
  persistedSearchParams: Array<keyof TSearchSchema>,
  exclude?: Array<keyof TSearchSchema>,
): SearchMiddleware<TSearchSchema> {

Then you would use it like this and safely save only the intended params.

It’s not the best DX, but since we have both the persistedSearchParams param and the typed exclude list, it’s a reasonable balance.

Screenshot 2025-08-21 at 1 08 17

Copy link

@coderabbitai coderabbitai bot left a 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 the router 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 environments

If 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 builds

Avoid 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 links

Using 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: Avoid as any on navigate by typing the updater

You 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: Drop as any on navigate

Type 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.

📥 Commits

Reviewing files that changed from the base of the PR and between 41d598f and fe839f2.

📒 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 covers src/; no changes needed

The project’s tsconfig.json doesn’t explicitly list an include, but by TypeScript’s defaults all .ts/.tsx files under the project (including src/) 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 when input === true is clear and consistent. No further action needed.


146-159: Typed getter looks good.

getSearch returns the per-route typed schema or null, 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 good

Merging 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 fix

It looks like there isn’t a lint script defined in your root or example package.json, so running pnpm -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 configured import/order rule.

examples/react/search-persistence/src/routes/products.tsx (1)

18-26: Middleware config matches intent

Persisting category/minPrice/maxPrice while excluding sortBy 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 sensible

Persisting name, status, and page while leaving limit transient is a reasonable default for UX.

Comment on lines +5 to +11
import { setupLocalStorageSync } from './utils/localStorage-sync'
import './styles.css'

// Setup localStorage sync for search persistence (optional)
// if (typeof window !== 'undefined') {
// setupLocalStorageSync()
// }
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

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

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

Apply this diff to clean up the unused pieces:

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

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

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

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

(unused-imports/no-unused-imports)

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

Comment on lines +84 to +107
<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>
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

⚠️ Potential issue

“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.

Comment on lines 232 to 285
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
}
}
Copy link

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 with store and exclude.
  • 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 so SearchPersistenceStore 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.

Comment on lines 261 to 281
// 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)
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

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.

Suggested change
// 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.

Add comprehensive search parameter persistence system with SSR support:

## Core Features
- **persistSearchParams middleware**: Automatically saves/restores search params per route
- **Explicit allowlists**: Required persistedSearchParams array with optional exclude
- **SSR-safe architecture**: Per-router store injection prevents cross-request state leakage
- **Smart restoration**: Bypasses validation for trusted restored data to prevent Zod defaults corruption

## Technical Implementation
- **SearchPersistenceStore**: Client-only store with subscribe/save/restore methods
- **Router integration**: Optional searchPersistenceStore injection via RouterOptions
- **Middleware bypass**: Restored data skips validation to prevent SSR hydration issues
- **Memory efficient**: Stores garbage collected per request in SSR environments

## Breaking Changes
- persistSearchParams now requires explicit persistedSearchParams array as first argument
- Removed auto-discovery from Zod schemas due to .optional().catch() permissiveness

## Added
- packages/router-core: SearchPersistenceStore class and persistence utilities
- examples/react/ssr-search-persistence: Complete SSR demo with server database integration
- docs: Comprehensive SSR documentation with architecture examples

## Fixed
- SSR validation timing issues causing restored values to be overridden by Zod defaults
- Cross-request state contamination in server environments
- Homepage navigation resetting persisted search parameters
- Hydration mismatches in SSR applications

Resolves search parameter persistence requirements for both client-only and SSR applications.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 12

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
examples/react/ssr-search-persistence/src/entry-client.tsx (1)

1-8: Hydration target is incorrect; pass a DOM element, not document.

hydrateRoot requires a container element (e.g., #root). Passing document will throw at runtime.

-import { hydrateRoot } from 'react-dom/client'
-import { RouterClient } from '@tanstack/react-router/ssr/client'
-import { createRouter } from './router'
-
-const router = createRouter()
-
-hydrateRoot(document, <RouterClient router={router} />)
+import { hydrateRoot } from 'react-dom/client'
+import { RouterClient } from '@tanstack/react-router/ssr/client'
+import { createRouter } from './router'
+
+const router = createRouter()
+const container = document.getElementById('root')
+if (!container) {
+  throw new Error('Root container #root not found')
+}
+hydrateRoot(container, <RouterClient router={router} />)
packages/router-core/src/searchMiddleware.ts (1)

36-56: stripSearchParams(true) short-circuits the chain and skips later middlewares (including the final merge).

Returning {} directly prevents downstream middlewares and the final step from running. Forward an empty search to next instead.

   return ({ search, next }) => {
     if (input === true) {
-      return {} as TSearchSchema
+      return next({} as TSearchSchema) as TSearchSchema
     }
     const result = next(search) as Record<string, unknown>
♻️ Duplicate comments (2)
docs/router/framework/react/api/router/persistSearchParamsFunction.md (1)

20-21: Grammar nit: add missing article (“an”).

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

-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
packages/router-core/src/router.ts (1)

2808-2820: Bug: default middleware wipes search when dest.search is undefined.

This regresses preservation semantics and breaks middlewares that expect the current search to flow through. Return the incoming search when dest.search is absent.

   const final = {
     middleware: ({ search }: { search: any }) => {
-      if (!dest.search) {
-        return {}
-      }
+      // No dest.search provided -> preserve current search
+      if (dest.search === undefined) {
+        return search
+      }
       if (dest.search === true) {
         return search
       }
       return functionalUpdate(dest.search, search)
     },
     route: { id: '', fullPath: '' },
   }
🧹 Nitpick comments (23)
examples/react/ssr-search-persistence/.gitignore (3)

1-2: Use trailing slashes to make directory intent explicit

Adding a trailing slash clarifies these are directories and avoids accidental matches of files named exactly "dist" or "node_modules".

-node_modules
-dist
+node_modules/
+dist/

3-3: Optionally broaden ignores for common Node/SSR artifacts (env, logs, TS cache, Vite)

If the SSR example uses TS/Vite or local env files, consider these additions to reduce accidental commits. Keep this scoped to the example folder.

 .DS_Store
+# Logs
+npm-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+
+# Env (keep a .env.example committed if needed)
+.env.local
+.env.development.local
+.env.test.local
+.env.production.local
+
+# TypeScript build info
+*.tsbuildinfo
+
+# Vite cache
+.vite/
+
+# Generic cache
+.cache/

1-3: Remove redundant .gitignore entries in the example

The repo’s top-level .gitignore already ignores node_modules, dist, and .DS_Store (recursively across all subdirectories), so the identical rules in examples/react/ssr-search-persistence/.gitignore are redundant. To reduce duplication, you can safely remove this local file (or at least drop those three lines). If you prefer examples to remain standalone when copied, keeping it is harmless but optional.

• Path for removal:

  • examples/react/ssr-search-persistence/.gitignore
examples/react/ssr-search-persistence/src/routerContext.tsx (1)

1-3: Broaden head typing and consider .ts extension.

  • If head is used to carry rendered head content for SSR, string may be too narrow. Typing it as ReactNode is safer and future‑proof (React 19 includes the new JSX runtime, which plays well with ReactNode).
  • Since there’s no JSX here, .ts would be a slightly cleaner extension (optional).

Apply:

+import type { ReactNode } from 'react'
 export interface RouterContext {
-  head: string
+  head: ReactNode
 }
examples/react/ssr-search-persistence/src/fetch-polyfill.js (1)

1-11: Avoid eagerly importing node-fetch when Node already provides fetch.

Node 18+ ships a Web Fetch API (via undici). You currently import node-fetch unconditionally, even if globalThis.fetch already exists. Switch to a conditional dynamic import so you only load node-fetch on older Node versions. This reduces startup cost and avoids bundling unused code.

Please confirm your minimum Node version for this example. The dynamic import below uses top‑level await, which requires Node ≥18 in ESM mode (this package is "type": "module").

-import fetch, { Headers, Request, Response } from 'node-fetch'
-
-// Polyfill fetch for Node.js environments that don't have it built-in
-// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
-globalThis.fetch = globalThis.fetch || fetch
-// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
-globalThis.Headers = globalThis.Headers || Headers
-// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
-globalThis.Request = globalThis.Request || Request
-// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
-globalThis.Response = globalThis.Response || Response
+// Polyfill fetch for Node.js environments that don't have it built-in
+/* eslint-disable @typescript-eslint/no-unnecessary-condition */
+if (!globalThis.fetch) {
+  const mod = await import('node-fetch')
+  globalThis.fetch = mod.default
+  globalThis.Headers = mod.Headers
+  globalThis.Request = mod.Request
+  globalThis.Response = mod.Response
+}
examples/react/ssr-search-persistence/tsconfig.json (1)

1-13: Improve SSR DX with Node types and explicit target.

  • Add "node" types so server‑side TS files (e.g., entry-server.tsx) can use Node globals without local type shims.
  • Set an explicit target for predictable output and editor tooling.
 {
   "compilerOptions": {
     "strict": true,
     "esModuleInterop": true,
     "jsx": "react-jsx",
     "module": "esnext",
-    "types": ["vite/client"],
+    "types": ["vite/client", "node"],
     "moduleResolution": "Bundler",
+    "target": "ES2022",
     "lib": ["DOM", "DOM.Iterable", "ES2022"],
     "skipLibCheck": true
   },
   "include": ["src", "vite.config.ts"]
 }
examples/react/ssr-search-persistence/package.json (1)

1-34: Declare Node engine and add a type‑check script.

  • Declaring an engine (≥18) documents the runtime expectation and aligns with SSR + native fetch.
  • Adding a typecheck script helps CI catch TS issues in the example.
  • Optional: a conventional start script can proxy to serve for familiarity.

If you intend to support Node <18 via the polyfill, set the engine accordingly and keep the polyfill. Otherwise, prefer Node ≥18.

 {
   "name": "tanstack-router-react-example-ssr-search-persistence",
   "private": true,
   "type": "module",
   "scripts": {
     "dev": "node server",
     "build": "pnpm run build:client && pnpm run build:server",
     "build:client": "vite build",
     "build:server": "vite build --ssr",
     "serve": "NODE_ENV=production node server",
-    "debug": "node --inspect-brk server"
+    "debug": "node --inspect-brk server",
+    "typecheck": "tsc --noEmit",
+    "start": "pnpm run serve"
   },
+  "engines": {
+    "node": ">=18"
+  },
   "dependencies": {
     "@tanstack/react-router": "^1.131.27",
     "@tanstack/router-plugin": "^1.131.27",
     "compression": "^1.8.0",
     "express": "^4.21.2",
     "get-port": "^7.1.0",
     "isbot": "^5.1.28",
     "node-fetch": "^3.3.2",
     "react": "^19.0.0",
     "react-dom": "^19.0.0",
     "zod": "^3.23.8"
   },
   "devDependencies": {
     "@tanstack/react-router-devtools": "^1.131.27",
     "@types/express": "^4.17.23",
     "@types/react": "^19.0.8",
     "@types/react-dom": "^19.0.1",
     "@vitejs/plugin-react": "^4.5.2",
     "typescript": "^5.8.3",
     "vite": "^6.3.5"
   }
 }
examples/react/ssr-search-persistence/README.md (2)

71-79: Fix markdownlint MD034 (bare URL).

Wrap the URL in angle brackets to satisfy MD034 and improve rendering.

-Then open http://localhost:3000
+Then open <http://localhost:3000>

82-89: Polish step wording for clarity and consistency.

Minor grammar/consistency tweaks: add conjunctions, prefer “the database”, and standardize “real time”.

-1. Navigate to Products, set some filters
-2. Go to Database tab - see your search params stored in real-time!
-3. Navigate to Users, set different filters
-4. Check Database again - both routes have isolated records
-5. Navigate back to Products - your filters persist from database!
-6. Refresh the page - everything restores correctly from database
-7. Each route maintains its own isolated search state in the database
+1. Navigate to Products and set some filters.
+2. Go to the Database tab — see your search params stored in real time.
+3. Navigate to Users and set different filters.
+4. Check the Database again — both routes have isolated records.
+5. Navigate back to Products — your filters persist from the database.
+6. Refresh the page — everything restores correctly from the database.
+7. Each route maintains its own isolated search state in the database.
docs/router/framework/react/api/router/persistSearchParamsFunction.md (2)

22-27: Clarify trigger semantics and tighten grammar/punctuation.

  • Make bullets parallel and explicit (avoid contractions, end with periods).
  • This improves scanability and addresses the grammar warnings flagged by tooling.
-- **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 an explicit search object: `<Link to="/users" search={{ name: 'John' }}>` → Middleware runs → No restoration (provided params take precedence).

254-270: Caveat: avoid calling the global getter in server code outside a request lifecycle.

If getSearchPersistenceStore() resolves a process/global-scoped store, calling it during SSR outside a per-request context could leak state across users. Suggest adding a cautionary note in the docs and preferring router-injected stores on the server.

 // Get the fully typed store instance
-const store = getSearchPersistenceStore()
+const store = getSearchPersistenceStore()
+
+// Server-side note:
+// Prefer a per-request store from your router instance during SSR to avoid cross-request leakage.
+// Use the global getter only in browser code or within a request-scoped context.
examples/react/ssr-search-persistence/src/entry-server.tsx (2)

65-74: Preserve multiple Set-Cookie headers; use res.append when available.

Some environments emit multiple Set-Cookie headers that must not be combined. Using setHeader can collapse or overwrite them.

-  response.headers.forEach((value, name) => {
-    res.setHeader(name, value)
-  })
+  // Preserve multi-valued headers (notably Set-Cookie)
+  response.headers.forEach((value, name) => {
+    if (name.toLowerCase() === 'set-cookie') {
+      // Express supports appending multiple Set-Cookie values
+      // In Node's fetch, Headers may expose getSetCookie(); fallback to setHeader otherwise.
+      const anyHeaders = response.headers as any
+      if (typeof anyHeaders.getSetCookie === 'function') {
+        for (const cookie of anyHeaders.getSetCookie()) {
+          res.append('Set-Cookie', cookie)
+        }
+      } else {
+        res.append('Set-Cookie', value)
+      }
+    } else {
+      res.setHeader(name, value)
+    }
+  })

56-64: Optional: wrap streaming in try/catch to surface pipeline errors clearly.

Improves observability in examples and prevents unhandled rejection logs during abrupt disconnects.

-  const response = await handler(({ responseHeaders, router }) =>
-    renderRouterToString({
-      responseHeaders,
-      router,
-      children: <RouterServer router={router} />,
-    }),
-  )
+  let response
+  try {
+    response = await handler(({ responseHeaders, router }) =>
+      renderRouterToString({
+        responseHeaders,
+        router,
+        children: <RouterServer router={router} />,
+      }),
+    )
+  } catch (err) {
+    // Log and rethrow or map to a 500 as needed for the example
+    throw err
+  }
examples/react/ssr-search-persistence/src/router.tsx (3)

14-22: Type-safety: avoid as any for route IDs when seeding from the DB.

If possible, validate that record.routeId exists in the route tree before saving, or defer seeding until after the router is created so you can consult router.routesById.

Example (post-router creation):

for (const record of allRecords) {
  if (router.routesById[record.routeId]) {
    searchPersistenceStore!.saveSearch(record.routeId as never, record.searchParams)
  }
}

23-35: Reduce redundant writes to the “database”.

Every store mutation currently writes each non-empty route snapshot. Consider diffing vs. the last persisted snapshot to avoid unnecessary saves.

I can sketch a small prevState cache if you want.


24-26: Minor: redundant null-check inside the subscriber.

searchPersistenceStore cannot be falsy inside this closure. Safe to drop the guard.

- searchPersistenceStore.subscribe(() => {
-   if (!searchPersistenceStore) return
+ searchPersistenceStore.subscribe(() => {
examples/react/ssr-search-persistence/server.js (1)

65-76: Head extraction is “best effort”; guard against missing tags.

indexOf('<head>') returning -1 will produce unexpected substrings. Low risk in dev, but a simple guard avoids noisy 500s when plugins change HTML.

- viteHead = viteHead.substring(
-   viteHead.indexOf('<head>') + 6,
-   viteHead.indexOf('</head>'),
- )
+ const start = viteHead.indexOf('<head>')
+ const end = viteHead.indexOf('</head>')
+ if (start !== -1 && end !== -1) {
+   viteHead = viteHead.substring(start + 6, end)
+ }
examples/react/ssr-search-persistence/src/routes/users.tsx (1)

29-31: Optional UX: reset page to 1 when filters change.

Users expect pagination to reset when a filter changes. Implement a small helper to reset page when name or status updates.

-  const updateSearch = (updates: Partial<UsersSearchSchema>) => {
-    navigate({ search: { ...search, ...updates } })
-  }
+  const updateSearch = (
+    updates: Partial<UsersSearchSchema>,
+    { resetPage }: { resetPage?: boolean } = {},
+  ) => {
+    navigate({
+      search: {
+        ...search,
+        ...updates,
+        ...(resetPage ? { page: 1 } : {}),
+      },
+    })
+  }
@@
-              onChange={(e) => updateSearch({ name: e.target.value })}
+              onChange={(e) => updateSearch({ name: e.target.value }, { resetPage: true })}
@@
-              onChange={(e) =>
-                updateSearch({
-                  status: e.target.value as UsersSearchSchema['status'],
-                })
-              }
+              onChange={(e) =>
+                updateSearch(
+                  { status: e.target.value as UsersSearchSchema['status'] },
+                  { resetPage: true },
+                )
+              }

Also applies to: 52-60, 69-83

examples/react/ssr-search-persistence/src/routes/__root.tsx (2)

72-112: Consider preserving search on the Database link for parity.

Products/Users preserve search via search={(prev) => prev}. If you want consistent UX while inspecting server-persisted entries, add the same to Database.

             <Link
               to="/database"
               activeProps={{
                 className: 'font-bold text-blue-600',
               }}
               className="hover:text-blue-500"
+              search={(prev) => prev}
             >
               Database
             </Link>

26-52: Optional hardening: add SRI/CSP-friendly attributes to external Tailwind script.

When SSRing in stricter environments, adding integrity, crossOrigin, or switching to a locally hosted asset helps avoid CSP/subresource risks. Not required for the example, but worth noting.

examples/react/ssr-search-persistence/src/routes/database.tsx (2)

34-46: Clear router store more efficiently (and deterministically).

Instead of iterating over records (which might be stale at click time), clear the whole store in one call. If you intentionally only want to clear known records, use a functional state update to avoid stale closures.

   const clearAllRecords = () => {
     serverDatabase.clear()

-    // Also clear from router store
+    // Also clear from router store
     const searchStore = router.options.searchPersistenceStore
     if (searchStore) {
-      records.forEach((record) => {
-        searchStore.clearSearch(record.routeId)
-      })
+      // If the intent is to wipe all client-side persisted searches:
+      searchStore.clearAllSearches()
     }

     setRecords([])
   }

112-129: Fix potential stale state in delete handler.

Use a functional state update to avoid capturing an outdated records array in concurrent renders.

 onClick={() => {
   serverDatabase.delete(record.routeId, record.userId)

   const searchStore = router.options.searchPersistenceStore
   if (searchStore) {
     searchStore.clearSearch(record.routeId)
   }

-  setRecords(records.filter((r) => r !== record))
+  setRecords((prev) => prev.filter((r) => r !== record))
 }}
examples/react/ssr-search-persistence/src/routes/products.tsx (1)

8-13: Coerce numeric search params to be robust with URL string inputs.

Search params often arrive as strings; z.coerce.number() is safer than z.number() with .catch.

 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'),
 })
📜 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.

📥 Commits

Reviewing files that changed from the base of the PR and between fe839f2 and 6887082.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (25)
  • docs/router/framework/react/api/router/persistSearchParamsFunction.md (1 hunks)
  • examples/react/search-persistence/src/routes/__root.tsx (1 hunks)
  • examples/react/ssr-search-persistence/.gitignore (1 hunks)
  • examples/react/ssr-search-persistence/README.md (1 hunks)
  • examples/react/ssr-search-persistence/package.json (1 hunks)
  • examples/react/ssr-search-persistence/server.js (1 hunks)
  • examples/react/ssr-search-persistence/src/components/ClientOnly.tsx (1 hunks)
  • examples/react/ssr-search-persistence/src/entry-client.tsx (1 hunks)
  • examples/react/ssr-search-persistence/src/entry-server.tsx (1 hunks)
  • examples/react/ssr-search-persistence/src/fetch-polyfill.js (1 hunks)
  • examples/react/ssr-search-persistence/src/routeTree.gen.ts (1 hunks)
  • examples/react/ssr-search-persistence/src/router.tsx (1 hunks)
  • examples/react/ssr-search-persistence/src/routerContext.tsx (1 hunks)
  • examples/react/ssr-search-persistence/src/routes/__root.tsx (1 hunks)
  • examples/react/ssr-search-persistence/src/routes/database.tsx (1 hunks)
  • examples/react/ssr-search-persistence/src/routes/index.tsx (1 hunks)
  • examples/react/ssr-search-persistence/src/routes/products.tsx (1 hunks)
  • examples/react/ssr-search-persistence/src/routes/users.tsx (1 hunks)
  • examples/react/ssr-search-persistence/tsconfig.json (1 hunks)
  • examples/react/ssr-search-persistence/vite.config.ts (1 hunks)
  • packages/react-router/src/index.tsx (1 hunks)
  • packages/router-core/src/index.ts (1 hunks)
  • packages/router-core/src/route.ts (1 hunks)
  • packages/router-core/src/router.ts (7 hunks)
  • packages/router-core/src/searchMiddleware.ts (3 hunks)
✅ Files skipped from review due to trivial changes (1)
  • examples/react/ssr-search-persistence/src/routeTree.gen.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/router-core/src/route.ts
  • packages/react-router/src/index.tsx
  • examples/react/search-persistence/src/routes/__root.tsx
🧰 Additional context used
🧬 Code graph analysis (13)
examples/react/ssr-search-persistence/src/entry-server.tsx (3)
examples/react/ssr-search-persistence/src/router.tsx (1)
  • createRouter (8-62)
packages/router-core/src/searchMiddleware.ts (1)
  • SearchPersistenceStore (59-171)
examples/react/basic-ssr-file-based/src/entry-server.tsx (1)
  • render (10-61)
examples/react/ssr-search-persistence/server.js (1)
examples/react/basic-ssr-file-based/server.js (1)
  • createServer (7-86)
examples/react/ssr-search-persistence/src/components/ClientOnly.tsx (1)
packages/react-router/src/index.tsx (1)
  • ClientOnly (139-139)
examples/react/ssr-search-persistence/src/routes/users.tsx (2)
examples/react/ssr-search-persistence/src/routes/products.tsx (1)
  • Route (17-25)
packages/router-core/src/searchMiddleware.ts (1)
  • persistSearchParams (231-313)
examples/react/ssr-search-persistence/vite.config.ts (1)
examples/solid/basic-ssr-streaming-file-based/vite.config.js (3)
  • __filename (7-7)
  • ssrBuildConfig (11-25)
  • clientBuildConfig (28-41)
examples/react/ssr-search-persistence/src/entry-client.tsx (2)
examples/react/ssr-search-persistence/src/router.tsx (1)
  • createRouter (8-62)
packages/react-router/src/index.tsx (1)
  • createRouter (280-280)
examples/react/ssr-search-persistence/src/routes/database.tsx (4)
examples/react/ssr-search-persistence/src/routes/__root.tsx (1)
  • Route (11-55)
examples/react/ssr-search-persistence/src/routes/products.tsx (1)
  • Route (17-25)
examples/react/ssr-search-persistence/src/routes/users.tsx (1)
  • Route (17-23)
examples/react/ssr-search-persistence/src/components/ClientOnly.tsx (1)
  • ClientOnly (8-20)
examples/react/ssr-search-persistence/src/routes/__root.tsx (3)
examples/react/search-persistence/src/routes/__root.tsx (1)
  • Route (4-6)
packages/react-router/src/index.tsx (2)
  • Route (261-261)
  • createRootRouteWithContext (266-266)
examples/react/ssr-search-persistence/src/routerContext.tsx (1)
  • RouterContext (1-3)
examples/react/ssr-search-persistence/src/routes/index.tsx (4)
examples/react/ssr-search-persistence/src/routes/__root.tsx (1)
  • Route (11-55)
examples/react/ssr-search-persistence/src/routes/database.tsx (1)
  • Route (7-9)
examples/react/ssr-search-persistence/src/routes/products.tsx (1)
  • Route (17-25)
examples/react/ssr-search-persistence/src/routes/users.tsx (1)
  • Route (17-23)
examples/react/ssr-search-persistence/src/routes/products.tsx (3)
examples/react/ssr-search-persistence/src/routes/__root.tsx (1)
  • Route (11-55)
examples/react/ssr-search-persistence/src/routes/users.tsx (1)
  • Route (17-23)
packages/router-core/src/searchMiddleware.ts (1)
  • persistSearchParams (231-313)
examples/react/ssr-search-persistence/src/router.tsx (2)
packages/router-core/src/searchMiddleware.ts (1)
  • SearchPersistenceStore (59-171)
packages/router-core/src/router.ts (1)
  • Register (100-102)
packages/router-core/src/searchMiddleware.ts (3)
packages/router-core/src/route.ts (2)
  • AnyRoute (710-725)
  • SearchMiddleware (87-89)
packages/router-core/src/router.ts (1)
  • RegisteredRouter (104-104)
packages/router-core/src/utils.ts (1)
  • replaceEqualDeep (212-259)
packages/router-core/src/router.ts (3)
packages/router-core/src/searchMiddleware.ts (1)
  • SearchPersistenceStore (59-171)
packages/router-core/src/route.ts (2)
  • AnyRoute (710-725)
  • SearchMiddleware (87-89)
packages/router-core/src/utils.ts (1)
  • functionalUpdate (195-204)
🪛 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)


[grammar] ~244-~244: There might be a mistake here.
Context: ...ent-only store**: Store is only created on client-side (`typeof window !== 'undefi...

(QB_NEW_EN)

examples/react/ssr-search-persistence/README.md

[grammar] ~83-~83: There might be a mistake here.
Context: ...ate to Products, set some filters 2. Go to Database tab - see your search params s...

(QB_NEW_EN)


[grammar] ~83-~83: There might be a mistake here.
Context: ... your search params stored in real-time! 3. Navigate to Users, set different filters...

(QB_NEW_EN)


[grammar] ~85-~85: There might be a mistake here.
Context: ...erent filters 4. Check Database again - both routes have isolated records 5. Navigat...

(QB_NEW_EN)


[grammar] ~86-~86: There might be a mistake here.
Context: ...back to Products - your filters persist from database! 6. Refresh the page - everyth...

(QB_NEW_EN)


[grammar] ~86-~86: There might be a mistake here.
Context: ...ts - your filters persist from database! 6. Refresh the page - everything restores c...

(QB_NEW_EN)


[grammar] ~87-~87: There might be a mistake here.
Context: ...he page - everything restores correctly from database 7. Each route maintains its ow...

(QB_NEW_EN)

🪛 markdownlint-cli2 (0.17.2)
examples/react/ssr-search-persistence/README.md

78-78: Bare URL used

(MD034, no-bare-urls)

🔇 Additional comments (10)
examples/react/ssr-search-persistence/.gitignore (1)

1-3: LGTM for a minimal example ignore list

Ignoring node_modules, dist, and .DS_Store is sensible for an app example and avoids unnecessary churn in the example directory.

examples/react/ssr-search-persistence/README.md (1)

26-33: Confirm API name consistency: SearchPersistenceStore vs getSearchPersistenceStore().

The PR objectives mention a getSearchPersistenceStore() factory. The README instantiates new SearchPersistenceStore() on both server and client. Please confirm the intended public API and align the examples and docs accordingly to avoid confusion.

I can update the snippets once you confirm whether the factory should be preferred over direct construction.

examples/react/ssr-search-persistence/src/components/ClientOnly.tsx (1)

8-20: LGTM overall; simple SSR-safe mount gate.

The mount flag approach is straightforward and appropriate for hiding client-only UI during SSR until hydration completes.

packages/router-core/src/index.ts (1)

257-264: Public API re-exports look consistent; ensure framework packages mirror them.

Confirm packages/react-router/src/index.tsx also re-exports SearchPersistenceStore, createSearchPersistenceStore, persistSearchParams, and getSearchPersistenceStore, since the docs import them from @tanstack/react-router.

examples/react/ssr-search-persistence/src/entry-server.tsx (1)

41-52: Good: per-request SearchPersistenceStore injection prevents cross-request contamination.

This matches the “per-request isolation” guidance and avoids global state. No changes needed here.

examples/react/ssr-search-persistence/src/routes/index.tsx (1)

24-41: Example links look correct and type-safe with the target routes’ schemas.

Pre-filled search objects align with the Products/Users schemas and demonstrate persistence clearly. No issues spotted.

examples/react/ssr-search-persistence/src/router.tsx (1)

64-68: Module augmentation placement LGTM.

The Register augmentation matches TanStack Router patterns and will give you IntelliSense for router. No changes needed.

examples/react/ssr-search-persistence/src/routes/users.tsx (1)

8-16: Schema and types look solid.

Good defaults via catch, and the persisted keys list matches the UI explanation.

examples/react/ssr-search-persistence/vite.config.ts (1)

45-53: Config selection is clean and matches Vite’s isSsrBuild.

Plugin setup (router + React) and dual-build switching look good.

packages/router-core/src/router.ts (1)

935-939: Good: per-router client store initialization avoids SSR cross-request leakage.

Auto-creating SearchPersistenceStore in the browser while requiring explicit injection on the server is the right trade-off for safety and DX.

Comment on lines +28 to +33
## Restoration Behavior

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

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

Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

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

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

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

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

📝 Committable suggestion

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

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

Comment on lines +189 to +206
### Basic SSR Setup

```tsx
import { createRouter, SearchPersistenceStore } from '@tanstack/react-router'
import { routeTree } from './routeTree.gen'

export function createAppRouter() {
// Create isolated store per router instance (per SSR request)
const searchPersistenceStore =
typeof window !== 'undefined' ? new SearchPersistenceStore() : undefined

return createRouter({
routeTree,
searchPersistenceStore, // Inject the store
// ... other options
})
}
```
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

SSR “Basic Setup” contradicts the per-request isolation claim; fix sample to create the store on the server too.

The comment says “per SSR request,” but the code creates a store only on the client. Align the example with the server entry that injects a per-request store.

-export function createAppRouter() {
-  // Create isolated store per router instance (per SSR request)
-  const searchPersistenceStore =
-    typeof window !== 'undefined' ? new SearchPersistenceStore() : undefined
-
-  return createRouter({
-    routeTree,
-    searchPersistenceStore, // Inject the store
-    // ... other options
-  })
-}
+export function createAppRouter() {
+  // Create a new store per router instance.
+  // On the server, ensure this is done per request.
+  const searchPersistenceStore = new SearchPersistenceStore()
+
+  return createRouter({
+    routeTree,
+    searchPersistenceStore, // Inject the store (server: per request; client: per hydration)
+    // ... other options
+  })
+}

Follow-up: ensure the server entry (e.g., your render handler) creates a fresh router per request and does not reuse a store across requests.

📝 Committable suggestion

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

Suggested change
### Basic SSR Setup
```tsx
import { createRouter, SearchPersistenceStore } from '@tanstack/react-router'
import { routeTree } from './routeTree.gen'
export function createAppRouter() {
// Create isolated store per router instance (per SSR request)
const searchPersistenceStore =
typeof window !== 'undefined' ? new SearchPersistenceStore() : undefined
return createRouter({
routeTree,
searchPersistenceStore, // Inject the store
// ... other options
})
}
```
import { createRouter, SearchPersistenceStore } from '@tanstack/react-router'
import { routeTree } from './routeTree.gen'
export function createAppRouter() {
// Create a new store per router instance.
// On the server, ensure this is done per request.
const searchPersistenceStore = new SearchPersistenceStore()
return createRouter({
routeTree,
searchPersistenceStore, // Inject the store (server: per request; client: per hydration)
// ... other options
})
}
🤖 Prompt for AI Agents
In docs/router/framework/react/api/router/persistSearchParamsFunction.md around
lines 189 to 206, the SSR "Basic Setup" example contradicts the "per SSR
request" claim by only instantiating SearchPersistenceStore on the client;
change the sample to always create a new SearchPersistenceStore instance for
each router creation (so the server path also constructs a fresh store per
request) and update the example to show the server/render entry creating a fresh
router (and store) per request rather than reusing a global/shared store across
requests.

Comment on lines +242 to +248
### SSR Considerations

- **Client-only store**: Store is only created on client-side (`typeof window !== 'undefined'`)
- **Hydration-safe**: No server/client mismatch issues
- **Performance**: Restored data bypasses validation to prevent SSR timing issues
- **Memory efficient**: Stores are garbage collected per request

Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Revise “Client-only store” bullet; it conflicts with the server example and per-request isolation.

Rephrase to reflect recommended usage: create a new store per SSR request on the server; create one on the client during hydration.

-- **Client-only store**: Store is only created on client-side (`typeof window !== 'undefined'`)
- - **Hydration-safe**: No server/client mismatch issues
+- **Per-request server store**: On the server, create a new `SearchPersistenceStore` per request and pass it to the router instance.
+- **Client-side hydration**: On the client, create a `SearchPersistenceStore` during hydration (or reuse one injected via SSR) to continue from server state.
+ - **Hydration-safe**: No server/client mismatch issues
📝 Committable suggestion

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

Suggested change
### SSR Considerations
- **Client-only store**: Store is only created on client-side (`typeof window !== 'undefined'`)
- **Hydration-safe**: No server/client mismatch issues
- **Performance**: Restored data bypasses validation to prevent SSR timing issues
- **Memory efficient**: Stores are garbage collected per request
### SSR Considerations
- **Per-request server store**: On the server, create a new `SearchPersistenceStore` per request and pass it to the router instance.
- **Client-side hydration**: On the client, create a `SearchPersistenceStore` during hydration (or reuse one injected via SSR) to continue from server state.
- **Hydration-safe**: No server/client mismatch issues
- **Performance**: Restored data bypasses validation to prevent SSR timing issues
- **Memory efficient**: Stores are garbage collected per request
🧰 Tools
🪛 LanguageTool

[grammar] ~244-~244: There might be a mistake here.
Context: ...ent-only store**: Store is only created on client-side (`typeof window !== 'undefi...

(QB_NEW_EN)

🤖 Prompt for AI Agents
In docs/router/framework/react/api/router/persistSearchParamsFunction.md around
lines 242 to 248, the "Client-only store" bullet is misleading because the docs
show a server example and recommend per-request isolation; update that bullet to
state that a new store should be created per SSR request on the server and a
separate store created on the client during hydration, making clear server-side
stores are per-request (garbage collected after the request) and the client
creates its own store on hydration to avoid cross-request/state leakage.

Comment on lines +98 to +104
if (!isTest) {
createServer().then(async ({ app }) =>
app.listen(await getPort({ port: portNumbers(3000, 3100) }), () => {
console.info('Client Server: http://localhost:3000')
}),
)
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Log the actual port you bind to.

You request a free port in [3000–3100] but always log 3000. Capture and log the resolved port.

Apply this diff:

-if (!isTest) {
-  createServer().then(async ({ app }) =>
-    app.listen(await getPort({ port: portNumbers(3000, 3100) }), () => {
-      console.info('Client Server: http://localhost:3000')
-    }),
-  )
-}
+if (!isTest) {
+  createServer().then(async ({ app }) => {
+    const port = await getPort({ port: portNumbers(3000, 3100) })
+    app.listen(port, () => {
+      console.info(`Client Server: http://localhost:${port}`)
+    })
+  })
+}
📝 Committable suggestion

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

Suggested change
if (!isTest) {
createServer().then(async ({ app }) =>
app.listen(await getPort({ port: portNumbers(3000, 3100) }), () => {
console.info('Client Server: http://localhost:3000')
}),
)
}
if (!isTest) {
createServer().then(async ({ app }) => {
const port = await getPort({ port: portNumbers(3000, 3100) })
app.listen(port, () => {
console.info(`Client Server: http://localhost:${port}`)
})
})
}
🤖 Prompt for AI Agents
In examples/react/ssr-search-persistence/server.js around lines 98–104, the code
requests a free port in the 3000–3100 range but always logs
"http://localhost:3000"; change it to capture the resolved port value from
getPort and log that. Specifically, await getPort(...) into a variable (e.g.,
port), pass that variable to app.listen, and use it in the console.info message
so the actual bound port is printed.

Comment on lines +1 to +6
import { useEffect, useState } from 'react'

interface ClientOnlyProps {
children: React.ReactNode
fallback?: React.ReactNode
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Type-only React import missing; React.ReactNode will not resolve.

Import ReactNode as a type or import type React to reference React.ReactNode.

-import { useEffect, useState } from 'react'
+import { useEffect, useState, type ReactNode } from 'react'
@@
-interface ClientOnlyProps {
-  children: React.ReactNode
-  fallback?: React.ReactNode
-}
+interface ClientOnlyProps {
+  children: ReactNode
+  fallback?: ReactNode
+}
📝 Committable suggestion

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

Suggested change
import { useEffect, useState } from 'react'
interface ClientOnlyProps {
children: React.ReactNode
fallback?: React.ReactNode
}
import { useEffect, useState, type ReactNode } from 'react'
interface ClientOnlyProps {
children: ReactNode
fallback?: ReactNode
}
🤖 Prompt for AI Agents
In examples/react/ssr-search-persistence/src/components/ClientOnly.tsx around
lines 1 to 6, the file uses React.ReactNode but does not import React as a type,
so the type will not resolve; update the imports to include the type (either add
"import type React from 'react'" and keep React.ReactNode, or import ReactNode
directly via "import { useEffect, useState, type ReactNode } from 'react'" and
change the interface to use ReactNode) to ensure the type resolves correctly.

Comment on lines +115 to +120
<button
onClick={() => navigate({ search: {} })}
className="bg-red-500 text-white px-4 py-2 rounded hover:bg-red-600"
>
Clear All Filters
</button>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Clear-all UX bug: persisted values are immediately restored. Also clear the per-route store.

persistSearchParams restores when the incoming allowed set is empty, so navigate({ search: {} }) will bring saved filters back. Clear the per-route entry before navigating.

-  <button
-    onClick={() => navigate({ search: {} })}
+  <button
+    onClick={() => {
+      const store = router.options.searchPersistenceStore
+      // Route id for this page is '/products'
+      store?.clearSearch('/products' as any)
+      navigate({ search: {} })
+    }}
     className="bg-red-500 text-white px-4 py-2 rounded hover:bg-red-600"
   >
     Clear All Filters

Additionally, import and use the router:

-import {
-  createFileRoute,
-  persistSearchParams,
-  useNavigate,
-} from '@tanstack/react-router'
+import {
+  createFileRoute,
+  persistSearchParams,
+  useNavigate,
+  useRouter,
+} from '@tanstack/react-router'
@@
 function ProductsComponent() {
-  const search = Route.useSearch()
-  const navigate = useNavigate()
+  const search = Route.useSearch()
+  const navigate = useNavigate()
+  const router = useRouter()
📝 Committable suggestion

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

Suggested change
<button
onClick={() => navigate({ search: {} })}
className="bg-red-500 text-white px-4 py-2 rounded hover:bg-red-600"
>
Clear All Filters
</button>
// ─── At the top of src/routes/products.tsx ─────────────────────────────────────
-import {
- createFileRoute,
- persistSearchParams,
- useNavigate,
import {
createFileRoute,
persistSearchParams,
useNavigate,
useRouter,
} from '@tanstack/react-router'
// ─── Inside your ProductsComponent ─────────────────────────────────────────────
function ProductsComponent() {
const search = Route.useSearch()
const navigate = useNavigate()
const router = useRouter()
return (
<div>
{/* ... other UI ... */}
<button
onClick={() => {
const store = router.options.searchPersistenceStore
// Route id for this page is '/products'
store?.clearSearch('/products' as any)
navigate({ search: {} })
}}
className="bg-red-500 text-white px-4 py-2 rounded hover:bg-red-600"
>
Clear All Filters
</button>
{/* ... remaining UI ... */}
</div>
)
}
🤖 Prompt for AI Agents
In examples/react/ssr-search-persistence/src/routes/products.tsx around lines
115-120, the "Clear All Filters" button calls navigate({ search: {} }) but
persisted search params are immediately restored and the per-route store is not
cleared; import and use the router instance, call the per-route persistence
clear method (remove the persisted entry for this route) before calling
navigate, and then navigate to an empty search; also ensure you pass an explicit
empty allowed set or use the router API that prevents rehydration so
persistSearchParams will not restore values when the allowed set is empty.

Comment on lines +131 to +137
<button
type="button"
onClick={() => navigate({ search: {} })}
className="bg-red-500 text-white px-4 py-2 rounded hover:bg-red-600"
>
Clear All Filters
</button>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

“Clear All Filters” does not clear persisted values; middleware restores them.

persistSearchParams restores saved keys when the current search is empty, so navigate({ search: {} }) won’t clear persistence. Clear the route’s persisted entry before navigating.

Apply this diff:

-import {
-  createFileRoute,
-  persistSearchParams,
-  useNavigate,
-} from '@tanstack/react-router'
+import {
+  createFileRoute,
+  persistSearchParams,
+  useNavigate,
+  useRouter,
+} from '@tanstack/react-router'
@@
 function UsersComponent() {
   const search = Route.useSearch()
   const navigate = useNavigate()
+  const router = useRouter()
@@
-          <button
+          <button
             type="button"
-            onClick={() => navigate({ search: {} })}
+            onClick={() => {
+              // Clear persisted values for this route, then reset URL search
+              router.options.searchPersistenceStore?.clearSearch(Route.id)
+              navigate({ search: {} })
+            }}
             className="bg-red-500 text-white px-4 py-2 rounded hover:bg-red-600"
           >
             Clear All Filters
           </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.

Suggested change
<button
type="button"
onClick={() => navigate({ search: {} })}
className="bg-red-500 text-white px-4 py-2 rounded hover:bg-red-600"
>
Clear All Filters
</button>
// examples/react/ssr-search-persistence/src/routes/users.tsx
import {
createFileRoute,
persistSearchParams,
useNavigate,
useRouter,
} from '@tanstack/react-router'
function UsersComponent() {
const search = Route.useSearch()
const navigate = useNavigate()
const router = useRouter()
return (
// …other UI…
<button
type="button"
onClick={() => {
// Clear persisted values for this route, then reset URL search
router.options.searchPersistenceStore?.clearSearch(Route.id)
navigate({ search: {} })
}}
className="bg-red-500 text-white px-4 py-2 rounded hover:bg-red-600"
>
Clear All Filters
</button>
// …other UI…
)
}
🤖 Prompt for AI Agents
In examples/react/ssr-search-persistence/src/routes/users.tsx around lines 131
to 137, the Clear All Filters button only calls navigate({ search: {} }) but
persisted search params middleware re-inserts saved values; clear the route's
persisted storage entry before navigating. Remove the saved search key used by
persistSearchParams (e.g. localStorage.removeItem(<the-route-key>) or call the
persistence helper/clear function for the "users" route) and then call
navigate({ search: {} }) so the middleware finds no saved state to restore.

import { tanstackRouter } from '@tanstack/router-plugin/vite'
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import type { BuildEnvironmentOptions } from 'vite'
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Type import likely incorrect; use Vite’s BuildOptions.

BuildEnvironmentOptions isn’t part of Vite’s public types. Use BuildOptions instead.

-import type { BuildEnvironmentOptions } from 'vite'
+import type { BuildOptions } from 'vite'
📝 Committable suggestion

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

Suggested change
import type { BuildEnvironmentOptions } from 'vite'
import type { BuildOptions } from 'vite'
🤖 Prompt for AI Agents
In examples/react/ssr-search-persistence/vite.config.ts around line 6, the code
imports a non-existent Vite type BuildEnvironmentOptions; replace that import
with Vite’s correct BuildOptions type (import type { BuildOptions } from 'vite')
and update any variable/type annotations that reference BuildEnvironmentOptions
to use BuildOptions instead so the config uses the public Vite type.

Comment on lines +12 to +26
const ssrBuildConfig: BuildEnvironmentOptions = {
ssr: true,
outDir: 'dist/server',
ssrEmitAssets: true,
copyPublicDir: false,
emptyOutDir: true,
rollupOptions: {
input: path.resolve(__dirname, 'src/entry-server.tsx'),
output: {
entryFileNames: '[name].js',
chunkFileNames: 'assets/[name]-[hash].js',
assetFileNames: 'assets/[name]-[hash][extname]',
},
},
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Use valid Vite build options; drop emitAssets.

emitAssets is not a recognized Vite build option. The rest looks fine.

-const ssrBuildConfig: BuildEnvironmentOptions = {
+const ssrBuildConfig: BuildOptions = {
   ssr: true,
   outDir: 'dist/server',
   ssrEmitAssets: true,
   copyPublicDir: false,
   emptyOutDir: true,
   rollupOptions: {
     input: path.resolve(__dirname, 'src/entry-server.tsx'),
     output: {
       entryFileNames: '[name].js',
       chunkFileNames: 'assets/[name]-[hash].js',
       assetFileNames: 'assets/[name]-[hash][extname]',
     },
   },
 }
 
-const clientBuildConfig: BuildEnvironmentOptions = {
+const clientBuildConfig: BuildOptions = {
   outDir: 'dist/client',
-  emitAssets: true,
   copyPublicDir: true,
   emptyOutDir: true,
   rollupOptions: {
     input: path.resolve(__dirname, 'src/entry-client.tsx'),
     output: {
       entryFileNames: 'static/[name].js',
       chunkFileNames: 'static/assets/[name]-[hash].js',
       assetFileNames: 'static/assets/[name]-[hash][extname]',
     },
   },
 }

Also applies to: 29-42

🤖 Prompt for AI Agents
In examples/react/ssr-search-persistence/vite.config.ts around lines 12 to 26
(and similarly lines 29 to 42), remove the invalid ssrEmitAssets build option
(it is not a recognized Vite build option); keep the other valid options (ssr,
outDir, copyPublicDir, emptyOutDir, rollupOptions) and if you need SSR asset
emission behavior, use the supported Vite options (e.g., generate an SSR
manifest or adjust rollup output) instead of ssrEmitAssets.

Comment on lines 231 to 313
export function persistSearchParams<TSearchSchema>(
persistedSearchParams: Array<keyof TSearchSchema>,
exclude?: Array<keyof TSearchSchema>,
): SearchMiddleware<TSearchSchema> {
return ({ search, next, route, router }) => {
const store = router.options.searchPersistenceStore as
| SearchPersistenceStore
| undefined

if (!store) {
return next(search)
}

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

const savedSearch = store.getSearch(route.id)
const 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) {
// Skip router validation for restored data since we know it's valid
// This prevents Zod .catch() defaults from overriding our restored values
const result = savedSearch as TSearchSchema

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),
),
)

if (Object.keys(filteredResult).length > 0) {
store.saveSearch(route.id, filteredResult)
}

return result
}
}

const result = next(searchToProcess)

// 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),
),
)

// Only save if we have actual search params to persist
// Don't save empty objects as they overwrite existing data
if (Object.keys(filteredResult).length > 0) {
store.saveSearch(route.id, filteredResult)
}

return result
}
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Persistence middleware drops unrelated keys and can’t clear stale entries. Merge, don’t filter input to next, and always save (empty clears).

  • Passing only filtered keys to next discards unrelated search keys from other matched routes.
  • Early “restore” returns only the saved subset, dropping other keys.
  • Saving is gated on non-empty results, so you can’t clear persisted state.

Refactor to:

  • Keep search intact for next, overlay restored allowed keys when appropriate.
  • Apply exclude to what you save, not to the input you pass along.
  • Always call saveSearch (empty payload clears).
-export function persistSearchParams<TSearchSchema>(
+export function persistSearchParams<TSearchSchema extends object>(
   persistedSearchParams: Array<keyof TSearchSchema>,
   exclude?: Array<keyof TSearchSchema>,
 ): SearchMiddleware<TSearchSchema> {
   return ({ search, next, route, router }) => {
     const store = router.options.searchPersistenceStore as
       | SearchPersistenceStore
       | undefined

     if (!store) {
       return next(search)
     }

-    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
-
-    const savedSearch = store.getSearch(route.id)
-    const searchToProcess = filteredSearch
+    const allowedKeysStr = persistedSearchParams.map((key) => String(key))
+    const excludeKeys = exclude ? exclude.map((key) => String(key)) : []
+    const searchRecord = search as Record<string, unknown>
+    const incomingAllowed = Object.fromEntries(
+      Object.entries(searchRecord).filter(([key]) => allowedKeysStr.includes(key)),
+    ) as Record<string, unknown>
+
+    const savedSearch = store.getSearch(route.id) as Record<string, unknown> | null
+    let mergedInput = search

-    if (savedSearch && Object.keys(savedSearch).length > 0) {
-      const currentSearch = filteredSearch as Record<string, unknown>
+    if (savedSearch && Object.keys(savedSearch).length > 0) {
+      const currentSearch = incomingAllowed
       const isEmpty = Object.keys(currentSearch).length === 0

       if (isEmpty) {
-        // Skip router validation for restored data since we know it's valid
-        // This prevents Zod .catch() defaults from overriding our restored values
-        const result = savedSearch as TSearchSchema
-
-        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),
-          ),
-        )
-
-        if (Object.keys(filteredResult).length > 0) {
-          store.saveSearch(route.id, filteredResult)
-        }
-
-        return result
+        // Restore only allowed keys and overlay onto the existing search
+        const restoredAllowed = Object.fromEntries(
+          Object.entries(savedSearch).filter(
+            ([key]) => allowedKeysStr.includes(key) && !excludeKeys.includes(key),
+          ),
+        ) as Record<string, unknown>
+
+        // Save back (even if empty => clears) and continue through the chain
+        store.saveSearch(route.id, restoredAllowed as any)
+        mergedInput = { ...(search as any), ...restoredAllowed } as TSearchSchema
       }
     }

-    const result = next(searchToProcess)
+    const result = next(mergedInput)

     // 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),
-      ),
+      Object.entries(resultRecord).filter(([key]) => allowedKeysStr.includes(key)),
     )

-    const excludeKeys = exclude ? exclude.map((key) => String(key)) : []
     const filteredResult = Object.fromEntries(
       Object.entries(paramsToSave).filter(
         ([key]) => !excludeKeys.includes(key),
       ),
     )

-    // Only save if we have actual search params to persist
-    // Don't save empty objects as they overwrite existing data
-    if (Object.keys(filteredResult).length > 0) {
-      store.saveSearch(route.id, filteredResult)
-    }
+    // Always save; empty payload clears the entry via SearchPersistenceStore
+    store.saveSearch(route.id, filteredResult as any)

     return result
   }
 }
📝 Committable suggestion

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

Suggested change
export function persistSearchParams<TSearchSchema>(
persistedSearchParams: Array<keyof TSearchSchema>,
exclude?: Array<keyof TSearchSchema>,
): SearchMiddleware<TSearchSchema> {
return ({ search, next, route, router }) => {
const store = router.options.searchPersistenceStore as
| SearchPersistenceStore
| undefined
if (!store) {
return next(search)
}
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
const savedSearch = store.getSearch(route.id)
const 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) {
// Skip router validation for restored data since we know it's valid
// This prevents Zod .catch() defaults from overriding our restored values
const result = savedSearch as TSearchSchema
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),
),
)
if (Object.keys(filteredResult).length > 0) {
store.saveSearch(route.id, filteredResult)
}
return result
}
}
const result = next(searchToProcess)
// 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),
),
)
// Only save if we have actual search params to persist
// Don't save empty objects as they overwrite existing data
if (Object.keys(filteredResult).length > 0) {
store.saveSearch(route.id, filteredResult)
}
return result
}
}
export function persistSearchParams<TSearchSchema extends object>(
persistedSearchParams: Array<keyof TSearchSchema>,
exclude?: Array<keyof TSearchSchema>,
): SearchMiddleware<TSearchSchema> {
return ({ search, next, route, router }) => {
const store = router.options.searchPersistenceStore as
| SearchPersistenceStore
| undefined
if (!store) {
return next(search)
}
// Build allow-list and exclude-list once
const allowedKeysStr = persistedSearchParams.map((key) => String(key))
const excludeKeys = exclude ? exclude.map((key) => String(key)) : []
// Capture all incoming params, but note which are allowed
const searchRecord = search as Record<string, unknown>
const incomingAllowed = Object.fromEntries(
Object.entries(searchRecord).filter(([key]) =>
allowedKeysStr.includes(key),
),
) as Record<string, unknown>
// Fetch persisted state
const savedSearch = store.getSearch(route.id) as
| Record<string, unknown>
| null
// Prepare the input we’ll pass to next()
let mergedInput = search as TSearchSchema
if (savedSearch && Object.keys(savedSearch).length > 0) {
const isEmpty = Object.keys(incomingAllowed).length === 0
if (isEmpty) {
// Only restore the allowed subset, minus any excludes
const restoredAllowed = Object.fromEntries(
Object.entries(savedSearch).filter(
([key]) =>
allowedKeysStr.includes(key) && !excludeKeys.includes(key),
),
) as Record<string, unknown>
// Persist it back (even an empty object clears prior state)
store.saveSearch(route.id, restoredAllowed as any)
// Merge restored keys back into the full search object
mergedInput = {
...(search as any),
...restoredAllowed,
} as TSearchSchema
}
}
// Proceed with the full (or merged) search object
const result = next(mergedInput)
// At the end, pick out only the allowed keys, apply excludes, and always save
const resultRecord = result as Record<string, unknown>
const paramsToSave = Object.fromEntries(
Object.entries(resultRecord).filter(([key]) =>
allowedKeysStr.includes(key),
),
)
const filteredResult = Object.fromEntries(
Object.entries(paramsToSave).filter(
([key]) => !excludeKeys.includes(key),
),
)
// Always call saveSearch; an empty filteredResult will clear the stored entry
store.saveSearch(route.id, filteredResult as any)
return result
}
}
🤖 Prompt for AI Agents
In packages/router-core/src/searchMiddleware.ts around lines 231 to 313, the
middleware currently filters the incoming search and calls next(filtered) and
returns restored results, which drops unrelated keys from other routes and
prevents clearing persisted state because save is skipped for empty objects;
change it to call next with the original search (or a merged object that
overlays restored allowed keys onto the original search when restoring), stop
returning early with only the saved subset, and always call
store.saveSearch(route.id, filteredSaved) (allowing an empty object to clear
persisted data); apply exclude only when building the object to save, not to the
input passed to next, and ensure the returned result preserves all original keys
merged with any validated/normalized values from next.

- Changed package name from 'tanstack-router-react-example-basic-file-based'
  to 'tanstack-router-react-example-search-persistence'
- Resolves NX project graph conflict that prevented running tests
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (3)
examples/react/search-persistence/src/routes/index.tsx (1)

94-105: “Clean Navigation” still restores persisted params — clear the store before navigating (repeat of prior feedback).

Replace the Links with buttons that clear the persisted entry, then navigate. Also import getSearchPersistenceStore and useNavigate.

- 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

examples/react/search-persistence/src/routes/users.tsx (1)

152-158: Reset immediately rehydrates persisted params — clear the store first (repeat of prior feedback).

Clear the persisted entry for /users, then navigate to {}.

-import {
-  createFileRoute,
-  useNavigate,
-  persistSearchParams,
-  retainSearchParams,
-  stripSearchParams,
-  Link,
-  Outlet,
-  useLocation,
-} from '@tanstack/react-router'
+import {
+  createFileRoute,
+  getSearchPersistenceStore,
+  Link,
+  Outlet,
+  persistSearchParams,
+  retainSearchParams,
+  stripSearchParams,
+  useLocation,
+  useNavigate,
+} 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>

Also applies to: 1-10

packages/router-core/src/searchMiddleware.ts (1)

288-291: Always save (empty payload clears stale state)

Skipping save on empty results leaves old values persisted indefinitely. SearchPersistenceStore.saveSearch already deletes the entry for empty payloads—call it unconditionally.

-      if (Object.keys(filteredResult).length > 0) {
-        store.saveSearch(storageKey, filteredResult)
-      }
+      // Always save; empty clears prior state.
+      store.saveSearch(storageKey, filteredResult)
🧹 Nitpick comments (11)
examples/react/search-persistence/src/routes/index.tsx (2)

36-38: Nit: wording contradicts the section title.

Change “Clean navigation - … restores …” to “Auto-restore — … restores …”.

-              Clean navigation - middleware automatically restores saved
-              parameters
+              Auto-restore — middleware automatically restores saved parameters

1-1: Fix import member order per ESLint.

Keep specifiers alphabetized to satisfy sort-imports.

- import { createFileRoute, Link } from '@tanstack/react-router'
+ import { createFileRoute, Link } from '@tanstack/react-router'

(Above is already ordered; the earlier change adds members in sorted order.)

packages/router-core/src/route.ts (1)

74-84: Type safety: avoid any for searchPersistenceStore; define a minimal interface.

Prevents untyped usage without introducing a runtime import cycle.

+// Minimal interface to avoid type import cycles
+export interface SearchPersistenceStoreLike {
+  getSearch: (key: string) => any
+  saveSearch: (key: string, search: Record<string, unknown>) => void
+  clearSearch: (key: string) => void
+}
+
 export interface SearchMiddlewareRouter {
   options: {
-    searchPersistenceStore?: any // Avoid circular dependency, will be typed at usage
+    searchPersistenceStore?: SearchPersistenceStoreLike
   }
examples/react/search-persistence/src/routes/users.tsx (2)

4-4: Import member order: ‘persistSearchParams’ should be alphabetized.

Keep specifiers sorted to satisfy sort-imports.

(Handled in the import diff above.)


106-110: Remove unnecessary as any.

navigate({ search: updater | object }) is typed; the cast hides real issues.

-    } as any)
+    })
packages/router-core/src/router.ts (1)

58-66: Use the exported SearchMiddlewareRouter type for stronger typing.

Aligns applySearchMiddleware’s router param with the public middleware context and avoids ad-hoc shapes.

-  SearchMiddleware,
-  SearchMiddlewareContext,
+  SearchMiddleware,
+  SearchMiddlewareContext,
+  SearchMiddlewareRouter,
 } from './route'
-function applySearchMiddleware({
+function applySearchMiddleware({
   search,
   dest,
   destRoutes,
   _includeValidateSearch,
-  router,
+  router,
   currentLocationMatches,
   destPathname,
 }: {
   search: any
   dest: BuildNextOptions
   destRoutes: Array<AnyRoute>
   _includeValidateSearch: boolean | undefined
-  router: { options: { searchPersistenceStore?: any } }
+  router: SearchMiddlewareRouter
   currentLocationMatches: Array<any>
   destPathname: string
 }) {

Also applies to: 2747-2750

examples/react/search-persistence/src/routes/users.$userId.tsx (2)

1-6: Alphabetize import members.

Keeps lint green.

-import {
-  createFileRoute,
-  retainSearchParams,
-  persistSearchParams,
-  Link,
-} from '@tanstack/react-router'
+import {
+  createFileRoute,
+  Link,
+  persistSearchParams,
+  retainSearchParams,
+} from '@tanstack/react-router'

146-156: Simplify tab update and future-proof with an updater.

Avoid rebuilding the whole search object; merge with previous.

-  const updateTab = (tab: UserDetailSearchSchema['tab']) => {
-    const newSearch = {
-      name: search.name,
-      status: search.status,
-      page: search.page,
-      tab: tab,
-    }
-
-    navigate({
-      search: newSearch,
-    })
-  }
+  const updateTab = (tab: UserDetailSearchSchema['tab']) => {
+    navigate({
+      search: (prev) => ({ ...prev, tab }),
+    })
+  }
packages/router-core/src/searchMiddleware.ts (3)

233-233: Preserve generics in the returned middleware object

Return SearchMiddlewareObject<TSearchSchema> instead of any to keep type inference for route-specific search schemas.

-): SearchMiddlewareObject<any> {
+): SearchMiddlewareObject<TSearchSchema> {

259-268: Consider excludes when dropping keys on “no saved preferences”

When no saved prefs exist, you delete all persistedSearchParams. If exclude is meant to denote non-persisted-but-still-owned keys, confirm this behavior. If excluded keys shouldn’t be stripped, filter them out here.

-        persistedSearchParams.forEach((key) => {
+        persistedSearchParams
+          .filter((k) => !(exclude ?? []).map(String).includes(String(k)))
+          .forEach((key) => {
           delete searchWithoutOwnedParams[String(key)]
         })

37-41: Optional: run next with an empty object when stripping all

Returning {} early bypasses any route-level validation/defaulting. If you intend defaults to apply, forward {} to next.

-    if (input === true) {
-      return {} as any
-    }
+    if (input === true) {
+      return next({} as any)
+    }
📜 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.

📥 Commits

Reviewing files that changed from the base of the PR and between 320ba6e and ea29c02.

📒 Files selected for processing (10)
  • 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/users.$userId.tsx (1 hunks)
  • examples/react/search-persistence/src/routes/users.tsx (1 hunks)
  • packages/router-core/src/route.ts (1 hunks)
  • packages/router-core/src/router.ts (16 hunks)
  • packages/router-core/src/searchMiddleware.ts (2 hunks)
  • port-tanstack-router-e2e-react-basic-esbuild-file-based-external.txt (1 hunks)
  • port-tanstack-router-e2e-react-basic-esbuild-file-based.txt (1 hunks)
✅ Files skipped from review due to trivial changes (2)
  • port-tanstack-router-e2e-react-basic-esbuild-file-based.txt
  • port-tanstack-router-e2e-react-basic-esbuild-file-based-external.txt
🚧 Files skipped from review as they are similar to previous changes (1)
  • examples/react/search-persistence/src/routes/__root.tsx
🧰 Additional context used
🧬 Code graph analysis (7)
examples/react/search-persistence/src/routes/users.$userId.tsx (2)
examples/react/search-persistence/src/routes/users.tsx (1)
  • Route (23-33)
packages/router-core/src/searchMiddleware.ts (2)
  • retainSearchParams (11-31)
  • persistSearchParams (230-296)
examples/react/search-persistence/src/routes/users.tsx (2)
examples/react/search-persistence/src/routes/users.$userId.tsx (1)
  • Route (18-27)
packages/router-core/src/searchMiddleware.ts (3)
  • retainSearchParams (11-31)
  • persistSearchParams (230-296)
  • stripSearchParams (33-56)
packages/router-core/src/router.ts (3)
packages/router-core/src/searchMiddleware.ts (1)
  • SearchPersistenceStore (58-170)
packages/router-core/src/route.ts (4)
  • RouteContextOptions (958-966)
  • AnyRoute (725-740)
  • SearchMiddleware (102-104)
  • SearchMiddlewareContext (86-91)
packages/router-core/src/utils.ts (1)
  • functionalUpdate (195-204)
packages/router-core/src/route.ts (1)
packages/router-core/src/index.ts (2)
  • SearchMiddlewareContext (125-125)
  • SearchMiddleware (126-126)
examples/react/search-persistence/src/routeTree.gen.ts (1)
packages/router-generator/tests/generator/types-disabled/routeTree.snapshot.js (1)
  • UsersUserIdRoute (27-31)
packages/router-core/src/searchMiddleware.ts (3)
packages/router-core/src/route.ts (3)
  • SearchMiddleware (102-104)
  • AnyRoute (725-740)
  • SearchMiddlewareObject (97-100)
packages/router-core/src/utils.ts (1)
  • replaceEqualDeep (212-259)
packages/router-core/src/router.ts (1)
  • RegisteredRouter (105-105)
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.$userId.tsx (1)
  • Route (18-27)
examples/react/search-persistence/src/routes/users.tsx (1)
  • Route (23-33)
examples/react/search-persistence/src/routes/products.tsx (1)
  • Route (18-26)
🪛 ESLint
examples/react/search-persistence/src/routes/users.$userId.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)

examples/react/search-persistence/src/routes/index.tsx

[error] 1-1: Member 'Link' of the import declaration should be sorted alphabetically.

(sort-imports)

🔇 Additional comments (3)
packages/router-core/src/route.ts (1)

93-105: Middleware typing looks good.

Union form enabling object config with inherit flag is clear and aligns with router usage.

packages/router-core/src/router.ts (1)

2848-2856: LGTM: default middleware now preserves search when dest.search is undefined.

Fix prevents accidental wiping of params.

packages/router-core/src/searchMiddleware.ts (1)

11-31: LGTM: retainSearchParams behavior

Merges result over incoming search for true, and fills missing keys otherwise. Uses replaceEqualDeep for structural sharing.

Comment on lines +32 to +36
const UsersUserIdRoute = UsersUserIdRouteImport.update({
id: '/$userId',
path: '/$userId',
getParentRoute: () => UsersRoute,
} as any)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix route id for nested user route to match full path

The UsersUserIdRoute id should be the full path (/users/$userId) to stay consistent with FileRoutesById and module augmentation. Using '/\$userId' will break linking/lookup by id.

-const UsersUserIdRoute = UsersUserIdRouteImport.update({
-  id: '/$userId',
-  path: '/$userId',
-  getParentRoute: () => UsersRoute,
-} as any)
+const UsersUserIdRoute = UsersUserIdRouteImport.update({
+  id: '/users/$userId',
+  path: '/$userId',
+  getParentRoute: () => UsersRoute,
+} as any)
📝 Committable suggestion

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

Suggested change
const UsersUserIdRoute = UsersUserIdRouteImport.update({
id: '/$userId',
path: '/$userId',
getParentRoute: () => UsersRoute,
} as any)
const UsersUserIdRoute = UsersUserIdRouteImport.update({
id: '/users/$userId',
path: '/$userId',
getParentRoute: () => UsersRoute,
} as any)
🤖 Prompt for AI Agents
In examples/react/search-persistence/src/routeTree.gen.ts around lines 32 to 36,
the UsersUserIdRoute is defined with id '/$userId' which should be the full
nested path '/users/$userId'; update the id field to '/users/$userId' (and keep
path as '/$userId') so the route id matches the full path used by FileRoutesById
and module augmentation, ensuring correct linking/lookup by id.

Comment on lines +1569 to +1571
// Routes without validateSearch get clean search (prevents contamination)
filteredSearch = {}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Don’t wipe search on routes without validateSearch.

Setting filteredSearch = {} drops retained/persisted params when the destination route has no validator. Preserve nextSearch instead to maintain previous semantics and enable parent middlewares.

-      } else {
-        // Routes without validateSearch get clean search (prevents contamination)
-        filteredSearch = {}
+      } else {
+        // No validateSearch: preserve current search so middlewares can operate
+        filteredSearch = nextSearch
       }
📝 Committable suggestion

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

Suggested change
// Routes without validateSearch get clean search (prevents contamination)
filteredSearch = {}
}
} else {
// No validateSearch: preserve current search so middlewares can operate
filteredSearch = nextSearch
}
🤖 Prompt for AI Agents
In packages/router-core/src/router.ts around lines 1569 to 1571, the code
currently sets filteredSearch = {} for routes without validateSearch which wipes
retained/persisted params; instead preserve nextSearch so retained params and
parent middlewares still receive the prior search. Change the assignment to keep
nextSearch (e.g., filteredSearch = nextSearch or a shallow copy of it) rather
than replacing with an empty object, and ensure you don't mutate the original
nextSearch when making the copy.

Comment on lines +235 to +247
middleware: ({ search, next, router }) => {
const store = router.options.searchPersistenceStore as
| SearchPersistenceStore
| undefined

if (!store) {
return next(search)
}

const storageKey = router.destPathname || ''

const savedSearch = store.getSearch(storageKey)

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Key persistence by destination route id, not pathname

Using router.destPathname stores per-URL-instance entries (e.g., /users/123), which diverges from the typed store contract (keys are route ids like /users/$userId). Persist by destination route id (fall back to current route id) to align with getSearchPersistenceStore.

-    middleware: ({ search, next, router }) => {
+    middleware: ({ search, next, router, route }) => {
@@
-      const storageKey = router.destPathname || ''
+      const storageKey =
+        (router as any).destRoute?.id ?? (route as any).id
📝 Committable suggestion

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

Suggested change
middleware: ({ search, next, router }) => {
const store = router.options.searchPersistenceStore as
| SearchPersistenceStore
| undefined
if (!store) {
return next(search)
}
const storageKey = router.destPathname || ''
const savedSearch = store.getSearch(storageKey)
middleware: ({ search, next, router, route }) => {
const store = router.options.searchPersistenceStore as
| SearchPersistenceStore
| undefined
if (!store) {
return next(search)
}
const storageKey =
(router as any).destRoute?.id ?? (route as any).id
const savedSearch = store.getSearch(storageKey)
🤖 Prompt for AI Agents
In packages/router-core/src/searchMiddleware.ts around lines 235 to 247, the
code is using router.destPathname as the persistence key which stores
per-URL-instance entries; change it to use the destination route id with a
fallback to the current route id (and finally an empty string) so keys align
with the typed store contract. Replace the storageKey assignment to derive from
router.destRouteId || router.currentRouteId || '' (ensuring the value is a
string and properly typed), then use that storageKey when calling
store.getSearch; adjust any types if needed to satisfy the
SearchPersistenceStore signature.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants