Skip to content

Conversation

naoya7076
Copy link
Contributor

@naoya7076 naoya7076 commented Apr 9, 2025

Proposal

Overview

This PR introduces the new useHistoryState hook and related functionality to enable type-safe history state management in TanStack Router.

Background

In the current implementation of TanStack Router, to pass type-safe state (History State) between routes that isn't part of the path or params, we need to define types globally. This leads to two main issues:

  1. Type pollution - types appear in places where they aren't needed
  2. Interface bloat - as more states are added, the global interface grows excessively

For example:

declare module '@tanstack/react-router' {
  interface HistoryState {
    name?: string;
    project?: MyProjectType;
  }
}

#284
When migrating from react-router to TanStack Router, developers want to be able to handle state in a type-safe manner, similar to how they could with react-router:

const history = useHistory();
history.replace({ pathname: '/', state: { name: 'Lily' }});
...
const location = useLocation();
// do something with location.state...

This PR addresses these issues by adding route-specific, type-safe history state management.

Changes

  • Added useHistoryState hook export in the main package index
  • Extended the FileRoute class to support state validation with TStateValidator generic parameter
  • Created a full example in examples/react/basic-history-state demonstrating usage patterns
  • The example shows how to:
    • Define state schemas using Zod
    • Pass state during navigation with Link and navigate

Key features of useHistoryState

  • Type-safe access to history state passed during navigation
  • Support for default values and optional properties
  • Ability to select specific state properties
  • Non-strict mode for accessing raw state object

Example usage

Users can define state validation on routes:

validateState: (input) =>
  z.object({
    color: z.enum(['white', 'red', 'green', 'blue']).default('white'),
    visited: z.boolean().default(false),
    message: z.string().optional(),
  }).parse(input),

Then use it in components:

// From route directly
const state = postRoute.useHistoryState()

// With select to extract specific values
const color = useHistoryState({
  from: '/posts/post',
  select: (state) => state.color
})

Summary by CodeRabbit

  • New Features

    • History-state support: useHistoryState hook (React & Solid), route.useHistoryState, route-level state validation, select & structuralSharing options, and state-error handling on matches.
  • Devtools

    • New "State Params" panel to inspect validated history state.
  • Documentation

    • Added useHistoryState API guide with examples and validation guidance.
  • Examples

    • New React Vite+Tailwind example demonstrating history-state usage.
  • Tests

    • Extensive type-level and runtime tests covering selection, validation, and edge cases.

This commit introduces a new `validateState` function to handle state validation within the router. It supports various validation methods, including standard validation and parsing, and integrates with route options to validate state during routing.
This commit introduces the TStateValidator type across various routing components, enhancing the validation capabilities for state management. The changes include updates to the createFileRoute and Route classes, as well as adjustments in related files to support the new state validation feature.
This commit introduces the `useHistoryState` hook and related types across various routing components, improving state management capabilities. Updates include modifications to the Route and createRoute functions to support the new state handling feature.
This commit introduces a new section in the BaseTanStackRouterDevtoolsPanel to display state parameters when available.
This commit introduces the `useHistoryState` hook along with its associated types, enhancing the state management capabilities within the router. The new implementation allows for more flexible state handling and selection options, improving overall functionality.
This commit introduces new TypeScript types related to the `useHistoryState` hook, improving type safety and flexibility in state management within the router. The new types facilitate better state selection and handling options, enhancing the overall functionality of the routing system.
…roved state selection

This commit refactoring the state selection logic to utilize `useRouterState`.
…resolution

This commit modifies the `ResolveUseHistoryState` and ` UseHistoryStateResult` types
…ryState

This commit updates the `useHistoryState` hook to utilize `useLocation` for state management instead of `useRouterState`, improving the clarity and efficiency of state selection logic.
@hleekeeper
Copy link

@hleekeeper

Currently, This PR(useHistoryState) doesn't support accessing route-specific history state types like HistoryState['/example'].

The only available approach currently is to manually define the type:

type ExampleRouteState = {
  color: string
  // Same structure as defined in validateState
}

You're correct that the current HistoryState is an aggregation of all route states, and there's no type-level API to extract specific route state types

I guess we can access the route-specific history state type by RouterType['routeTree']['/example']['types']['fullStateSchema']? But I may be wrong. I'll check it out when the PR is merged and get released. Anyway, thanks for the great work!

@naoya7076
Copy link
Contributor Author

@hleekeeper
Yes, your understanding is spot on.
In examples/react/basic-history-state/src/main.tsx, I tried to access the route-specific history-state type like this:

type DestinationRouteStateSchema = RouteById<
  (typeof router)['routeTree'],
  '/state-examples/destination'
>['types']['fullStateSchema'];

This resolves to:

type DestinationRouteStateSchema = {
  example: string;
  count: number;
  options: string[];
};

@naoya7076
Copy link
Contributor Author

Hi @schiller-manuel,

About 3 weeks ago, I have addressed all the feedback and replied your comments. All the requested changes have been implemented:

  • Refactored shared validation logic between validateState and validateSearch
  • Unified validator types to use generic approach
  • Added comprehensive type tests as requested

The PR is ready for re-review already. Could you please take another look when you have a chance? I appreciate your time and feedback.

Thanks!

@schiller-manuel
Copy link
Contributor

sorry for being slow to review. will do so soon

Copy link

pkg-pr-new bot commented Jul 11, 2025

More templates

@tanstack/arktype-adapter

npm i https://pkg.pr.new/TanStack/router/@tanstack/arktype-adapter@3967

@tanstack/directive-functions-plugin

npm i https://pkg.pr.new/TanStack/router/@tanstack/directive-functions-plugin@3967

@tanstack/eslint-plugin-router

npm i https://pkg.pr.new/TanStack/router/@tanstack/eslint-plugin-router@3967

@tanstack/history

npm i https://pkg.pr.new/TanStack/router/@tanstack/history@3967

@tanstack/react-router

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-router@3967

@tanstack/react-router-devtools

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-router-devtools@3967

@tanstack/react-router-ssr-query

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-router-ssr-query@3967

@tanstack/react-start

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-start@3967

@tanstack/react-start-client

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-start-client@3967

@tanstack/react-start-plugin

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-start-plugin@3967

@tanstack/react-start-server

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-start-server@3967

@tanstack/router-cli

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-cli@3967

@tanstack/router-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-core@3967

@tanstack/router-devtools

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-devtools@3967

@tanstack/router-devtools-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-devtools-core@3967

@tanstack/router-generator

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-generator@3967

@tanstack/router-plugin

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-plugin@3967

@tanstack/router-ssr-query-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-ssr-query-core@3967

@tanstack/router-utils

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-utils@3967

@tanstack/router-vite-plugin

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-vite-plugin@3967

@tanstack/server-functions-plugin

npm i https://pkg.pr.new/TanStack/router/@tanstack/server-functions-plugin@3967

@tanstack/solid-router

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-router@3967

@tanstack/solid-router-devtools

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-router-devtools@3967

@tanstack/solid-start

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-start@3967

@tanstack/solid-start-client

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-start-client@3967

@tanstack/solid-start-plugin

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-start-plugin@3967

@tanstack/solid-start-server

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-start-server@3967

@tanstack/start-client-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-client-core@3967

@tanstack/start-plugin-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-plugin-core@3967

@tanstack/start-server-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-server-core@3967

@tanstack/start-server-functions-client

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-server-functions-client@3967

@tanstack/start-server-functions-fetcher

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-server-functions-fetcher@3967

@tanstack/start-server-functions-server

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-server-functions-server@3967

@tanstack/start-storage-context

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-storage-context@3967

@tanstack/valibot-adapter

npm i https://pkg.pr.new/TanStack/router/@tanstack/valibot-adapter@3967

@tanstack/virtual-file-routes

npm i https://pkg.pr.new/TanStack/router/@tanstack/virtual-file-routes@3967

@tanstack/zod-adapter

npm i https://pkg.pr.new/TanStack/router/@tanstack/zod-adapter@3967

commit: 6af18d5

@hleekeeper
Copy link

any update on the PR's review?

Copy link
Contributor

coderabbitai bot commented Aug 16, 2025

Walkthrough

Adds a typed history-state system: new useHistoryState hooks (React/Solid), route-level useHistoryState helpers, TStateValidator generics and state schemas threaded through route types, runtime state validation/propagation, omitInternalKeys utility, devtools state panel, example app, and runtime + type tests.

Changes

Cohort / File(s) Summary
Documentation
docs/router/framework/react/api/router/useHistoryStateHook.md
New docs describing useHistoryState API, options (from, strict, shouldThrow, select, structuralSharing), return semantics, state validation example (zod), and usage snippets.
React example app (basic-history-state)
examples/react/basic-history-state/*, examples/react/basic-history-state/src/*, examples/react/basic-history-state/.vscode/settings.json
New Vite + React example showing validated history state with Tailwind, README, configs, entry HTML, VSCode exclusions, and source demonstrating useHistoryState with zod validation.
History utility
packages/history/src/index.ts
Adds omitInternalKeys(state) to remove internal history keys (keys starting with __ and key).
React Router: hooks & APIs
packages/react-router/src/useHistoryState.tsx, packages/react-router/src/index.tsx, packages/react-router/src/route.tsx, packages/react-router/src/fileRoute.ts
New useHistoryState hook implementation and types, exports hook/types, adds per-route useHistoryState methods, and threads TStateValidator through route factory/type generics.
Router Core: types & validation
packages/router-core/src/route.ts, packages/router-core/src/routeInfo.ts, packages/router-core/src/validators.ts, packages/router-core/src/link.ts, packages/router-core/src/Matches.ts, packages/router-core/src/index.ts, packages/router-core/src/typePrimitives.ts, packages/router-core/src/useHistoryState.ts, packages/router-core/src/router.ts, packages/router-core/src/RouterProvider.ts
Introduces state-schema types (StateSchemaInput, FullStateSchema, ResolveStateValidatorInput*), threads TStateValidator broadly, extends RouteMatch with state/_strictState/stateError, validates state during matching and in buildLocation (opt-in flag), improves link typing for route-specific state, adds StateParamError and re-exports new types.
Solid Router: hooks & APIs
packages/solid-router/src/useHistoryState.tsx, packages/solid-router/src/index.tsx, packages/solid-router/src/route.tsx, packages/solid-router/src/fileRoute.ts
Adds Solid implementation of useHistoryState, per-route useHistoryState methods, exposes hook in exports, and threads TStateValidator through types.
Devtools
packages/router-devtools-core/src/BaseTanStackRouterDevtoolsPanel.tsx, packages/router-devtools-core/package.json
Adds @tanstack/history dep, merges per-match _strictState, strips internal keys, and displays validated state in devtools UI.
React Router tests
packages/react-router/tests/useHistoryState.test.tsx, packages/react-router/tests/useHistoryState.test-d.tsx, packages/react-router/tests/Matches.test-d.tsx
Adds runtime and type tests for useHistoryState; updates match typings to include fullStateSchema.
Solid Router tests
packages/solid-router/tests/useHistoryState.test-d.tsx, packages/solid-router/tests/Matches.test-d.tsx
Adds type tests for Solid useHistoryState and updates match typings to include fullStateSchema.
Misc example configs & styling
examples/react/basic-history-state/package.json, .../postcss.config.mjs, .../tailwind.config.mjs, .../tsconfig*.json, .../vite.config.js, .../.gitignore, .../index.html, .../src/styles.css
New project tooling and styling config files for the example app (Vite, Tailwind, PostCSS, TS configs, gitignore).

Sequence Diagram(s)

sequenceDiagram
  participant UI as Component
  participant Hook as useHistoryState
  participant Match as useMatch
  participant Core as Router Core
  participant Route as Route Match

  UI->>Hook: call useHistoryState({ from, strict, shouldThrow, select })
  Hook->>Match: useMatch({ from, strict, shouldThrow, select })
  Match->>Core: resolve matches for `from`
  Core->>Route: build/lookup match (include preMatchState)
  Core->>Route: if validateState -> sanitize (omitInternalKeys) -> validate -> merge _strictState / stateError
  Note right of Route: match now includes state, _strictState, stateError
  Match-->>Hook: match (filtered state)
  Hook-->>UI: selected value or throw/undefined
Loading
sequenceDiagram
  participant Nav as navigate/link
  participant Core as buildLocation
  participant Routes as Destination Routes

  Nav->>Core: buildLocation(to={path,state}, _includeValidateState:true)
  Core->>Routes: apply validateState on dest routes (omitInternalKeys + validators)
  Routes-->>Core: merged validated state
  Core-->>Nav: location with validated state
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Suggested reviewers

  • schiller-manuel
  • Sheraff

Poem

A rabbit hops through routes so bright,
I prune the keys that hide from sight,
I ask each route, "Is this state true?"
Merge strict bits, show errors too.
Hooks, devtools, docs — a history in bloom. 🐇✨

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@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

🔭 Outside diff range comments (1)
packages/solid-router/src/fileRoute.ts (1)

174-184: Add missing useHistoryState to LazyRoute interface augmentation

The LazyRoute interface declared in the module augmentation for @tanstack/router-core is out of sync with the class API—it exposes useHistoryState on the class but not in the augmentation. Consumers relying on the augmented interface won’t get typings for this hook.

Files to update:

  • packages/solid-router/src/fileRoute.ts (lines ~174–184)
  • packages/react-router/src/fileRoute.ts (lines ~174–184)

Suggested diff:

 declare module '@tanstack/router-core' {
   export interface LazyRoute<in out TRoute extends AnyRoute> {
     useMatch: UseMatchRoute<TRoute['id']>
     useRouteContext: UseRouteContextRoute<TRoute['id']>
     useSearch: UseSearchRoute<TRoute['id']>
+    useHistoryState: UseHistoryStateRoute<TRoute['id']>
     useParams: UseParamsRoute<TRoute['id']>
     useLoaderDeps: UseLoaderDepsRoute<TRoute['id']>
     useLoaderData: UseLoaderDataRoute<TRoute['id']>
     useNavigate: () => UseNavigateResult<TRoute['fullPath']>
   }
 }
♻️ Duplicate comments (3)
packages/router-core/src/validators.ts (1)

75-87: Nice unification: shared schema-input resolver for search and state

This removes duplication and keeps the brand-based input erasure in one place. Matches the earlier review ask to reuse the type.

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

1201-1233: State validation path matches search validation semantics

Filtering internal keys via omitInternalKeys, merging strict results, and capturing stateError mirrors the search pipeline. This keeps match state coherent regardless of validation outcomes.

packages/solid-router/src/useHistoryState.tsx (1)

47-82: LGTM! Solid implementation mirrors React correctly.

The Solid implementation properly returns an Accessor and follows the same filtering and delegation pattern as the React version. The use of omitInternalKeys correctly addresses the centralization requirement from past reviews.

🧹 Nitpick comments (20)
examples/react/basic-history-state/README.md (1)

5-6: Update README to include pnpm and use the dev script

The example’s package.json defines a dev script (and also a generic start), so let’s standardize on dev and add pnpm:

In examples/react/basic-history-state/README.md (lines 5–6), replace:

- `npm install` or `yarn`
- `npm start` or `yarn start`
+ `npm install` or `pnpm install` or `yarn install`
+ `npm run dev` or `pnpm dev` or `yarn dev`

[optional_refactors_recommended]

examples/react/basic-history-state/tsconfig.dev.json (1)

1-10: Config looks solid; minor simplification optional

This is valid JSONC for a tsconfig and works fine. If you want to reduce redundancy, you can rely on just "include" and drop "files"; it’s functionally equivalent here.

Proposed edit:

{
   "composite": true,
   "extends": "../../../tsconfig.base.json",

-  "files": ["src/main.tsx"],
   "include": [
     "src"
     // "__tests__/**/*.test.*"
   ]
 }
examples/react/basic-history-state/.gitignore (1)

1-10: Consider ignoring generated routeTree if not committed

If routeTree.gen.ts is generated at dev time and not meant for version control, add it here. If you intend to commit it for type consumption/examples, keep as-is.

Proposed edit:

 node_modules
 .DS_Store
 dist
 dist-ssr
 *.local

 /test-results/
 /playwright-report/
 /blob-report/
 /playwright/.cache/
+**/routeTree.gen.ts
packages/history/src/index.ts (1)

99-112: Optional: return a narrower type via generics to improve DX

Today the function returns Record<string, unknown>, which forces downstream casts. You can keep the same runtime while improving static typing with a mapped type that erases internal keys from the return type.

Apply this diff:

 /**
  * Filters out internal state keys from a state object.
  * Internal keys are those that start with '__' or equal 'key'.
  */
-export function omitInternalKeys(
-  state: Record<string, unknown>,
-): Record<string, unknown> {
-  return Object.fromEntries(
-    Object.entries(state).filter(
-      ([key]) => !(key.startsWith('__') || key === 'key'),
-    ),
-  )
-}
+type OmitInternalKeys<T> = {
+  [K in keyof T as K extends `__${string}` | 'key' ? never : K]: T[K]
+}
+export function omitInternalKeys<T extends Record<string, unknown>>(
+  state: T,
+): OmitInternalKeys<T> {
+  return Object.fromEntries(
+    Object.entries(state).filter(
+      ([key]) => !(key.startsWith('__') || key === 'key'),
+    ),
+  ) as OmitInternalKeys<T>
+}

Note: This improves types for typical JSON-like state. If you rely on symbol keys, consider including them via Object.getOwnPropertySymbols to fully align runtime with types.

examples/react/basic-history-state/tsconfig.json (1)

1-12: Optional: add Vite types for better DX (import.meta and HMR)

Including vite/client types avoids ad-hoc ambient declarations when accessing import.meta.env or HMR APIs in the example.

Apply this diff:

   "compilerOptions": {
     "strict": true,
     "esModuleInterop": true,
     "jsx": "react-jsx",
     "target": "ESNext",
     "moduleResolution": "Bundler",
     "module": "ESNext",
     "lib": ["DOM", "DOM.Iterable", "ES2022"],
-    "skipLibCheck": true
+    "skipLibCheck": true,
+    "types": ["vite/client"]
   }
packages/router-core/src/validators.ts (1)

88-102: Confirm fallback behavior for non-branded function validators

When a validator is a plain function without the branded input, the resolver falls back to the function’s output type via ResolveValidatorOutputFn. For an “input-type” resolver, intuitively one might expect TInferredInput (or ResolveValidatorInputFn) instead.

  • If this mirrors existing search behavior and your type tests rely on output as the canonical “schema” in such cases, keep it.
  • If not intentional, consider switching the fallback to TInferredInput for function validators lacking the brand.

If you decide to pivot, the minimal change would be:

 export type ResolveSchemaValidatorInputFn<TValidator, TSchemaInput> =
   TValidator extends (input: infer TInferredInput) => any
     ? TInferredInput extends TSchemaInput
       ? Omit<TInferredInput, keyof TSchemaInput>
-      : ResolveValidatorOutputFn<TValidator>
+      : TInferredInput
     : AnySchema

Please confirm which behavior your useHistoryState / useSearch type tests expect for function validators without branded inputs.

examples/react/basic-history-state/index.html (1)

6-6: Use a more descriptive page title for the example

Clearer title improves discoverability and browser tab clarity.

-    <title>Vite App</title>
+    <title>TanStack Router – Basic History State</title>
examples/react/basic-history-state/src/styles.css (1)

8-10: Also target pseudo-elements for the global border color

This matches common Tailwind preflight patterns and avoids inconsistent borders on pseudo-elements.

-* {
-  @apply border-gray-200 dark:border-gray-800;
-}
+*, ::before, ::after {
+  @apply border-gray-200 dark:border-gray-800;
+}
examples/react/basic-history-state/package.json (1)

11-29: Move build-time CSS tooling to devDependencies and consider workspace linking for TanStack packages

  • Autoprefixer, PostCSS, and Tailwind are build-time only; shifting them to devDependencies keeps the runtime footprint cleaner.
  • Since this example lives in the monorepo and showcases unreleased features, using workspace:* helps ensure it resolves to the local workspace versions during development.

Move CSS tooling to devDependencies:

   "dependencies": {
-    "@tanstack/react-router": "^1.114.24",
-    "@tanstack/react-router-devtools": "^1.114.24",
-    "autoprefixer": "^10.4.20",
-    "postcss": "^8.5.1",
+    "@tanstack/react-router": "^1.114.24",
+    "@tanstack/react-router-devtools": "^1.114.24",
     "react": "^19.0.0",
     "react-dom": "^19.0.0",
     "redaxios": "^0.5.1",
-    "tailwindcss": "^3.4.17",
     "zod": "^3.24.2"
   },
   "devDependencies": {
+    "autoprefixer": "^10.4.20",
+    "postcss": "^8.5.1",
+    "tailwindcss": "^3.4.17",
     "@types/react": "^19.0.8",
     "@types/react-dom": "^19.0.3",
     "@vitejs/plugin-react": "^4.3.4",
     "typescript": "^5.7.2",
     "vite": "^6.1.0"
   }

Optionally, prefer local workspace versions for TanStack packages:

-    "@tanstack/react-router": "^1.114.24",
-    "@tanstack/react-router-devtools": "^1.114.24",
+    "@tanstack/react-router": "workspace:*",
+    "@tanstack/react-router-devtools": "workspace:*",
examples/react/basic-history-state/tailwind.config.mjs (1)

2-4: Optionally pin dark mode strategy

Your CSS uses dark: variants that work with Tailwind’s default 'media' strategy. Consider making it explicit for clarity; switch to 'class' if you intend a manual theme toggle.

 export default {
+  darkMode: 'media',
   content: ['./src/**/*.{js,jsx,ts,tsx}', './index.html'],
 }
packages/router-devtools-core/src/BaseTanStackRouterDevtoolsPanel.tsx (3)

233-242: State merge order and scope: consider precedence clarity and optionally include cachedMatches

getMergedStrictState merges pendingMatches first, then matches. If keys overlap across matches (e.g., parent vs child), later entries win. Assuming matches are ordered root→leaf, child keys will override. If that’s desired, great—otherwise document or reverse accordingly. Also, consider whether cachedMatches should be visible in this “State Params” view for parity with the “Cached Matches” section, or keep it limited to pending/active only (current behavior).

If you want to include cached matches, apply:

-  const matches = [
-    ...(routerState.pendingMatches ?? []),
-    ...routerState.matches,
-  ]
+  const matches = [
+    ...(routerState.pendingMatches ?? []),
+    ...routerState.matches,
+    // Optionally include cached matches to reflect all known strict state
+    // ...routerState.cachedMatches,
+  ]

294-299: omitInternalKeys is shallow — ensure no nested internal keys leak (optional)

omitInternalKeys only filters top-level "__" and "key" keys. If internal fields could appear nested (unlikely but possible), consider a deep omit helper similar to search sanitization patterns.

I can provide a minimal deep-omit helper if you decide to go that route.


345-346: Nit: validatedStateValue memo is redundant

validatedStateValue just aliases validatedState. You can pass validatedState directly to Explorer like you do for locationSearchValue to keep things consistent.

-    const validatedStateValue = createMemo(() => validatedState())
...
-              <Explorer
-                value={validatedStateValue}
+              <Explorer
+                value={validatedState}
packages/react-router/tests/useHistoryState.test-d.tsx (1)

1-533: Excellent type coverage for useHistoryState (strict vs loose, select, structural sharing, unions, root/index interactions)

These tests comprehensively exercise the new API surface and mirror useSearch behaviors. They should help catch regressions in validator wiring and state schema resolution.

Consider adding:

  • A case asserting the behavior when navigating directly to a route with a state validator but no state (validator receives {}, strict vs non-strict inferences).
  • A case verifying that selected sub-state preserves optionality in non-strict mode across multiple overlapping validators.
packages/react-router/tests/useHistoryState.test.tsx (2)

200-208: Nit: Test name vs. assertion are mismatched

The block comment suggests keeping the previous valid state visible, but the assertion checks for the invalid "yellow" value being rendered. Consider clarifying the test description or the assertion intent to avoid confusion for future readers.


302-338: Avoid TDZ pitfalls by defining postsRoute before its component uses it

PostsComponent closes over postsRoute which is declared later. It works at runtime because the component is invoked after assignment, but reordering to declare postsRoute first avoids temporal-dead-zone edge cases and improves readability.

examples/react/basic-history-state/src/main.tsx (1)

110-120: Optional: Demonstrate selecting or shaping state

Consider adding a small example using select and/or a derived value to showcase fine-grained subscriptions and structural sharing:

  • Example: stateExamplesRoute.useHistoryState({ select: s => s.count })
packages/router-core/src/router.ts (1)

1623-1641: Symmetry with search: Consider validating state in buildLocation on commit/hydration

You added gated validation for state (opts._includeValidateState) similar to search, but callers don’t pass it. For parity with search, consider propagating _includeValidateState: true in:

  • buildAndCommitLocation (alongside _includeValidateSearch: true)
  • Server beforeLoad preflight buildLocation call

This ensures committed history state reflects validated shapes when validators are present (mirrors search behavior). If the current behavior is intentional (eg. to keep unvalidated user fields), please confirm.

Proposed changes (outside this hunk):

  • In buildAndCommitLocation:
const location = this.buildLocation({
  ...(rest as any),
  _includeValidateSearch: true,
  _includeValidateState: true,
})
  • In beforeLoad:
const nextLocation = this.buildLocation({
  to: this.latestLocation.pathname,
  search: true,
  params: true,
  hash: true,
  state: true,
  _includeValidateSearch: true,
  _includeValidateState: true,
})
packages/react-router/src/useHistoryState.tsx (1)

85-100: Consider extracting repeated type casts for improved type safety.

The implementation correctly filters internal keys and delegates to useMatch. However, the double as any cast at the end could be improved for better type safety.

Consider extracting the type casting to improve readability and maintainability:

 export function useHistoryState<
   TRouter extends AnyRouter = RegisteredRouter,
   const TFrom extends string | undefined = undefined,
   TStrict extends boolean = true,
   TThrow extends boolean = true,
   TSelected = unknown,
   TStructuralSharing extends boolean = boolean,
 >(
   opts: UseHistoryStateOptions<
     TRouter,
     TFrom,
     TStrict,
     ThrowConstraint<TStrict, TThrow>,
     TSelected,
     TStructuralSharing
   >,
 ): ThrowOrOptional<
   UseHistoryStateResult<TRouter, TFrom, TStrict, TSelected>,
   TThrow
 > {
-  return useMatch({
+  const result = useMatch({
     from: opts.from!,
     strict: opts.strict,
     shouldThrow: opts.shouldThrow,
     structuralSharing: opts.structuralSharing,
     select: (match: any) => {
       const matchState = match.state
       const filteredState = omitInternalKeys(matchState)
       const typedState = filteredState as unknown as ResolveUseHistoryState<
         TRouter,
         TFrom,
         TStrict
       >
       return opts.select ? opts.select(typedState) : typedState
     },
-  } as any) as any
+  } as any)
+  
+  return result as ThrowOrOptional<
+    UseHistoryStateResult<TRouter, TFrom, TStrict, TSelected>,
+    TThrow
+  >
 }
packages/solid-router/src/route.tsx (1)

129-135: Consider reducing code duplication across route classes.

The useHistoryState implementation is duplicated across RouteApi, Route, and RootRoute classes with identical logic. This violates the DRY principle.

Consider extracting a shared helper function to reduce duplication:

+// Add near the top of the file after imports
+function createUseHistoryStateForRoute<TId>(id: TId): UseHistoryStateRoute<TId> {
+  return (opts?: any) => {
+    // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
+    return useHistoryState({
+      select: opts?.select,
+      from: id,
+    } as any) as any
+  }
+}

 export class RouteApi<
   TId,
   TRouter extends AnyRouter = RegisteredRouter,
 > extends BaseRouteApi<TId, TRouter> {
   // ... other methods ...

-  useHistoryState: UseHistoryStateRoute<TId> = (opts?: any) => {
-    // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
-    return useHistoryState({
-      select: opts?.select,
-      from: this.id,
-    } as any) as any
-  }
+  useHistoryState: UseHistoryStateRoute<TId> = createUseHistoryStateForRoute(this.id)

Apply the same pattern to the Route and RootRoute classes.

Also applies to: 277-283, 494-500

📜 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 72b65cf and 120ba3e.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (40)
  • docs/router/framework/react/api/router/useHistoryStateHook.md (1 hunks)
  • examples/react/basic-history-state/.gitignore (1 hunks)
  • examples/react/basic-history-state/.vscode/settings.json (1 hunks)
  • examples/react/basic-history-state/README.md (1 hunks)
  • examples/react/basic-history-state/index.html (1 hunks)
  • examples/react/basic-history-state/package.json (1 hunks)
  • examples/react/basic-history-state/postcss.config.mjs (1 hunks)
  • examples/react/basic-history-state/src/main.tsx (1 hunks)
  • examples/react/basic-history-state/src/styles.css (1 hunks)
  • examples/react/basic-history-state/tailwind.config.mjs (1 hunks)
  • examples/react/basic-history-state/tsconfig.dev.json (1 hunks)
  • examples/react/basic-history-state/tsconfig.json (1 hunks)
  • examples/react/basic-history-state/vite.config.js (1 hunks)
  • packages/history/src/index.ts (1 hunks)
  • packages/react-router/src/fileRoute.ts (9 hunks)
  • packages/react-router/src/index.tsx (4 hunks)
  • packages/react-router/src/route.tsx (27 hunks)
  • packages/react-router/src/useHistoryState.tsx (1 hunks)
  • packages/react-router/tests/Matches.test-d.tsx (7 hunks)
  • packages/react-router/tests/useHistoryState.test-d.tsx (1 hunks)
  • packages/react-router/tests/useHistoryState.test.tsx (1 hunks)
  • packages/router-core/src/Matches.ts (8 hunks)
  • packages/router-core/src/RouterProvider.ts (1 hunks)
  • packages/router-core/src/fileRoute.ts (7 hunks)
  • packages/router-core/src/index.ts (5 hunks)
  • packages/router-core/src/link.ts (2 hunks)
  • packages/router-core/src/route.ts (62 hunks)
  • packages/router-core/src/routeInfo.ts (1 hunks)
  • packages/router-core/src/router.ts (8 hunks)
  • packages/router-core/src/typePrimitives.ts (3 hunks)
  • packages/router-core/src/useHistoryState.ts (1 hunks)
  • packages/router-core/src/validators.ts (2 hunks)
  • packages/router-devtools-core/package.json (1 hunks)
  • packages/router-devtools-core/src/BaseTanStackRouterDevtoolsPanel.tsx (6 hunks)
  • packages/solid-router/src/fileRoute.ts (8 hunks)
  • packages/solid-router/src/index.tsx (4 hunks)
  • packages/solid-router/src/route.tsx (27 hunks)
  • packages/solid-router/src/useHistoryState.tsx (1 hunks)
  • packages/solid-router/tests/Matches.test-d.tsx (7 hunks)
  • packages/solid-router/tests/useHistoryState.test-d.tsx (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (18)
packages/history/src/index.ts (1)
packages/router-core/src/router.ts (1)
  • state (937-939)
packages/router-core/src/routeInfo.ts (2)
packages/router-core/src/index.ts (3)
  • AnyRoute (164-164)
  • ParseRoute (47-47)
  • PartialMergeAll (308-308)
packages/router-core/src/route.ts (1)
  • AnyRoute (785-801)
packages/react-router/tests/useHistoryState.test-d.tsx (1)
packages/router-core/src/router.ts (1)
  • state (937-939)
packages/react-router/tests/useHistoryState.test.tsx (2)
packages/react-router/src/route.tsx (3)
  • RouteComponent (605-605)
  • createRootRoute (542-580)
  • createRoute (318-387)
packages/router-core/src/router.ts (1)
  • state (937-939)
packages/react-router/src/useHistoryState.tsx (4)
packages/solid-router/src/useHistoryState.tsx (4)
  • UseHistoryStateBaseOptions (14-23)
  • UseHistoryStateOptions (25-32)
  • UseHistoryStateRoute (34-45)
  • useHistoryState (47-82)
packages/router-core/src/router.ts (3)
  • AnyRouter (693-693)
  • state (937-939)
  • RegisteredRouter (104-104)
packages/react-router/src/structuralSharing.ts (2)
  • ValidateSelected (40-47)
  • StructuralSharingOption (19-31)
packages/history/src/index.ts (1)
  • omitInternalKeys (103-111)
packages/solid-router/tests/useHistoryState.test-d.tsx (1)
packages/router-core/src/router.ts (1)
  • state (937-939)
packages/router-core/src/useHistoryState.ts (1)
packages/router-core/src/routeInfo.ts (1)
  • FullStateSchema (222-225)
packages/router-core/src/link.ts (4)
packages/router-core/src/index.ts (4)
  • NonNullableUpdater (299-299)
  • ResolveRelativePath (16-16)
  • RoutePaths (55-55)
  • RouteById (53-53)
packages/router-core/src/utils.ts (1)
  • NonNullableUpdater (90-92)
packages/history/src/index.ts (1)
  • ParsedHistoryState (55-59)
packages/router-core/src/routeInfo.ts (2)
  • RoutePaths (109-115)
  • RouteById (47-50)
packages/solid-router/src/useHistoryState.tsx (2)
packages/router-core/src/router.ts (3)
  • AnyRouter (693-693)
  • state (937-939)
  • RegisteredRouter (104-104)
packages/history/src/index.ts (1)
  • omitInternalKeys (103-111)
packages/router-devtools-core/src/BaseTanStackRouterDevtoolsPanel.tsx (1)
packages/history/src/index.ts (1)
  • omitInternalKeys (103-111)
packages/router-core/src/Matches.ts (1)
packages/router-core/src/routeInfo.ts (1)
  • FullStateSchema (222-225)
packages/router-core/src/router.ts (4)
packages/history/src/index.ts (1)
  • omitInternalKeys (103-111)
packages/router-core/src/utils.ts (1)
  • replaceEqualDeep (222-269)
packages/router-core/src/validators.ts (1)
  • AnyValidator (69-69)
packages/router-core/src/route.ts (1)
  • AnyRoute (785-801)
packages/router-core/src/validators.ts (1)
packages/router-core/src/route.ts (2)
  • SearchSchemaInput (48-50)
  • StateSchemaInput (52-54)
packages/react-router/src/fileRoute.ts (2)
packages/react-router/src/index.tsx (1)
  • useHistoryState (308-308)
packages/solid-router/src/index.tsx (1)
  • useHistoryState (315-315)
packages/react-router/src/route.tsx (1)
packages/router-core/src/route.ts (1)
  • RootRouteOptions (1352-1384)
packages/solid-router/tests/Matches.test-d.tsx (1)
packages/router-core/src/route.ts (1)
  • RootRoute (1803-1829)
packages/solid-router/src/fileRoute.ts (1)
packages/solid-router/src/useHistoryState.tsx (2)
  • UseHistoryStateRoute (34-45)
  • useHistoryState (47-82)
packages/router-core/src/route.ts (2)
packages/router-core/src/index.ts (10)
  • StateSchemaInput (118-118)
  • AnySchema (339-339)
  • IntersectAssign (296-296)
  • AnyStandardSchemaValidator (324-324)
  • AnyValidatorAdapter (334-334)
  • AnyValidatorObj (332-332)
  • ResolveStateValidatorInput (344-344)
  • NoInfer (284-284)
  • Constrain (309-309)
  • AnyValidator (338-338)
packages/router-core/src/utils.ts (3)
  • IntersectAssign (74-82)
  • NoInfer (5-5)
  • Constrain (131-133)
🪛 LanguageTool
docs/router/framework/react/api/router/useHistoryStateHook.md

[grammar] ~22-~22: There might be a mistake here.
Context: ...ed based on the route's validateState. - If false, the hook returns a loosely t...

(QB_NEW_EN)


[grammar] ~27-~27: There might be a mistake here.
Context: ... ### opts.shouldThrow option - Type: boolean - Optional - default: true - If false,...

(QB_NEW_EN)


[grammar] ~41-~41: There might be a mistake here.
Context: ...ngoption - Type:boolean` - Optional - Configures whether structural sharing is...

(QB_NEW_EN)


[grammar] ~42-~42: There might be a mistake here.
Context: ...bled for the value returned by select. - See the [Render Optimizations guide](../...

(QB_NEW_EN)

🪛 Biome (2.1.2)
packages/react-router/tests/useHistoryState.test.tsx

[error] 213-213: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.

For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level

(lint/correctness/useHookAtTopLevel)

packages/router-core/src/router.ts

[error] 2480-2480: Shouldn't redeclare 'routeNeedsPreload'. Consider to delete it or rename it.

'routeNeedsPreload' is defined here:

(lint/suspicious/noRedeclare)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Test
🔇 Additional comments (73)
examples/react/basic-history-state/postcss.config.mjs (1)

1-6: LGTM

Standard PostCSS/Tailwind setup using ESM config. No issues.

examples/react/basic-history-state/.vscode/settings.json (1)

1-11: LGTM

Excluding and read-only-protecting routeTree.gen.ts in the editor is a good practice for generated artifacts.

packages/history/src/index.ts (1)

99-112: Runtime sanitization looks correct and aligns with state shape

The function correctly strips both legacy key and internal __* keys, matching how ParsedHistoryState is constructed. This prevents leaking internal keys into consumer-facing selectors.

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

45-46: LGTM: BuildLocationFn now supports state validation inclusion

The new _includeValidateState?: boolean mirrors _includeValidateSearch and unblocks the router’s buildLocation path to compose validated state. Confirmed by related handling in router.ts.

Please verify that all call sites that set _includeValidateSearch also set _includeValidateState where appropriate (e.g., navigate vs. link prebuild) to keep behaviors consistent.

examples/react/basic-history-state/vite.config.js (1)

1-7: LGTM: Minimal, standard Vite + React config

This is sufficient for the example app and matches Vite’s recommended setup.

packages/router-devtools-core/package.json (1)

67-67: Dependency addition for history utilities looks correct

Adding @tanstack/history as a direct dependency aligns with the devtools usage (e.g., omitInternalKeys) and ensures consumers don’t need to install it separately.

examples/react/basic-history-state/package.json (1)

23-27: No changes needed: @vitejs/plugin-react 4.3.4 is fully compatible with Vite 6 & React 19

  • v4.3.4 changelog explicitly adds Vite 6 to peerDependencies.
  • Official create-vite React template ships with react ^19, vite ^6.x and plugin-react ^4.3.4.
  • Supports React 19 and the React Compiler workflow (follow Babel setup notes in the plugin docs if you opt into the compiler).
  • Ensure your Node.js version meets the plugin’s minimum requirement (v16+).
packages/solid-router/src/index.tsx (4)

55-55: LGTM! Consistent with the state management API expansion.

The export of StateSchemaInput aligns with the new history state functionality and follows the same pattern as the existing SearchSchemaInput export.


110-111: LGTM! State validator types properly exposed.

The exports of ResolveStateValidatorInput and ResolveStateValidatorInputFn complete the state validation type surface, following the established pattern of the search validator types.


315-315: LGTM! Hook properly exported for public use.

The useHistoryState hook export enables the core functionality of this PR in the Solid Router package.


345-345: LGTM! Validation type correctly exported.

The ValidateHistoryState type export completes the type-safe state validation API surface for Solid Router.

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

222-230: LGTM! Consistent implementation following established patterns.

The new FullStateSchema and FullStateSchemaInput type aliases correctly follow the same pattern as the existing FullSearchSchema and FullSearchSchemaInput types. They properly use ParseRoute to extract route information and PartialMergeAll to merge the state schemas across the route tree.

packages/router-core/src/typePrimitives.ts (3)

13-13: LGTM! Import added for history state support.

The UseHistoryStateResult import is correctly added to support the new history state validation types.


33-41: LGTM! History state validation type properly implemented.

The ValidateHistoryState type follows the same pattern as ValidateSearch and ValidateParams, correctly using the inference types (InferFrom, InferStrict, InferSelected) with UseHistoryStateResult.


193-201: LGTM! Result validation type follows established patterns.

The ValidateUseHistoryStateResult type mirrors the existing ValidateUseSearchResult and ValidateUseParamsResult patterns, providing consistent API design for history state validation.

packages/react-router/src/index.tsx (4)

56-56: LGTM! State schema input type properly exposed.

The StateSchemaInput type export enables type-safe state schema definitions in the public API.


112-113: LGTM! State validator types correctly exported.

The ResolveStateValidatorInput and ResolveStateValidatorInputFn exports provide the necessary type utilities for state validation in React Router.


308-308: LGTM! Hook properly exported for React usage.

The useHistoryState hook export enables the primary functionality of this PR in React Router.


342-342: LGTM! History state validation type correctly exposed.

The ValidateHistoryState type export completes the public API surface for type-safe history state validation in React Router.

docs/router/framework/react/api/router/useHistoryStateHook.md (8)

1-7: Clear and concise introduction.

The opening effectively explains the hook's purpose and functionality.


12-16: Well-documented from option.

The description clearly explains the optional route ID parameter and its fallback behavior.


18-24: Comprehensive strict option documentation.

The documentation clearly explains both strict and non-strict typing behavior, helping developers understand the trade-offs between type safety and flexibility.


26-31: Clear shouldThrow option explanation.

The documentation properly explains the error handling behavior and the alternative undefined return value.


32-37: Excellent select option documentation.

The explanation covers both the transformation functionality and the performance implications with shallow equality checks.


50-68: Great state validation section.

The section provides a practical example of using Zod for state validation, which aligns well with the broader ecosystem and demonstrates real-world usage patterns.


70-106: Comprehensive usage examples.

The examples cover all the major use cases: basic usage, route-specific state, route API usage, selection, and optional state handling. This provides excellent guidance for developers.


108-152: Excellent complete example.

The end-to-end example demonstrates the full workflow from route definition with validation, through navigation with state, to state consumption in components. This is particularly valuable for developers implementing the feature.

packages/router-devtools-core/src/BaseTanStackRouterDevtoolsPanel.tsx (3)

5-5: Import looks correct and aligns with new history-state sanitization

Bringing in omitInternalKeys from @tanstack/history is consistent with the new useHistoryState behavior across frameworks.


648-664: “State Params” Explorer integration looks solid

  • Uses sanitized, merged strict state
  • Mirrors Search Params UX with sensible defaultExpanded keys
  • Feature-gated behind hasState()

Looks good.


99-116: Route generic parameters align
Verified that the two undefined entries in BaseTanStackRouterDevtoolsPanel.tsx correspond to the new TSearchValidator and TStateValidator slots in packages/router-core/src/route.ts, and that all subsequent type arguments follow the same ordering. No changes required.

packages/solid-router/src/fileRoute.ts (2)

80-116: FileRoute generics and options updated to carry TStateValidator — good alignment

Adding TStateValidator alongside TSearchValidator through FileBaseRouteOptions, UpdatableRouteOptions, and the Route return type matches the core’s state-validator extension.


220-226: LazyRoute.useHistoryState implementation mirrors existing hooks — good

Delegating to useHistoryState with from: this.options.id and select passthrough matches the pattern used by useSearch/useParams/etc.

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

114-121: Re-exporting StateSchemaInput alongside SearchSchemaInput is consistent with the new validator surface

Good to see StateSchemaInput made part of the public types. Matches usage in tests.


343-349: State validator input type exports are complete

ResolveStateValidatorInput(Fn) exports will be useful for adapter authors and advanced type-level workflows.


359-363: New history-state type exports wired correctly

Exporting UseHistoryStateResult and ResolveUseHistoryState from core makes framework hooks strongly typed.


404-426: ValidateHistoryState and ValidateUseHistoryStateResult added — aligns with typePrimitives

Publicly exposing these maintains parity with existing ValidateSearch/ValidateUseSearchResult types.

packages/router-core/src/useHistoryState.ts (2)

5-13: UseHistoryStateResult conditional on TSelected is correct

The “unknown extends TSelected” pattern effectively detects the default generic and swaps in the resolved state shape when no custom selection type is provided.


14-21: Ignore the proposed TFrom constraint on ResolveUseHistoryState

The core type must continue to accept TFrom = undefined (the default “closest‐match” case). Introducing

TFrom extends string ?  : never

would make ResolveUseHistoryState<Router, undefined, true> resolve to never, breaking the default hook signature. Framework hooks already constrain TFrom in their own generics, so no change is needed here.

Likely an incorrect or invalid review comment.

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

438-450: Typed navigation state per target route: LGTM

The conditional typing for state based on ResolveRelativePath and RoutePaths/RouteById is precise and falls back correctly when the path can’t be resolved at compile time. Nice work keeping this purely type-level with no runtime cost.

packages/solid-router/tests/useHistoryState.test-d.tsx (1)

1-517: Type-surface coverage looks solid

Good breadth of assertions across root/index/nested routes, strict vs. non-strict, and overlapping state params. This should prevent regressions in the advanced conditional types powering useHistoryState.

examples/react/basic-history-state/src/main.tsx (1)

70-86: State validation example: LGTM

Clear zod-based validator and downstream consumption via route.useHistoryState(). This is a great minimal example.

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

2433-2471: Shared validation helper: Nice consolidation

The validateInput helper and thin wrappers for validateState/validateSearch remove duplication and centralize error construction. Clear improvement over the previous split logic.

packages/react-router/src/fileRoute.ts (3)

9-9: LGTM! Clean integration of the useHistoryState hook.

The imports are properly organized and follow the existing pattern.

Also applies to: 37-37


80-82: LGTM! Type parameters properly extended for state validation.

The TStateValidator generic parameter is correctly added to the FileRoute.createRoute method signature and properly threaded through all dependent type definitions, maintaining consistency with the existing TSearchValidator pattern.

Also applies to: 95-95, 109-109, 123-123


224-231: LGTM! Clean implementation of useHistoryState on LazyRoute.

The implementation follows the established pattern for other hooks in the class. The from parameter is correctly bound to the route's ID, and the optional parameters are properly forwarded.

packages/router-core/src/Matches.ts (2)

7-7: LGTM! State schema types properly integrated into RouteMatch.

The TFullStateSchema type parameter is correctly added to the RouteMatch interface and consistently propagated through all related type constructors (MakeRouteMatchFromRoute, MakeRouteMatch, MakeRouteMatchUnion). The implementation mirrors the existing pattern for TFullSearchSchema.

Also applies to: 123-123, 162-163, 217-217, 236-238, 248-248, 259-259


140-140: No action needed—stateError is correctly set on validation failure

The router’s state validation block returns the caught error as stateError (router.ts lines 1184–1203) and assigns it to match.stateError (router.ts lines 1357–1359), matching the unknown-typed field in Matches.ts.

packages/solid-router/src/route.tsx (2)

15-15: LGTM! Clean integration of useHistoryState imports and declarations.

The imports and module declarations properly extend the route API with the new hook.

Also applies to: 46-46, 77-77


179-179: LGTM! State validator properly propagated through type hierarchy.

The TStateValidator generic parameter is correctly added and threaded through all route-related type definitions, maintaining consistency with the existing pattern.

Also applies to: 196-196, 214-214, 236-236, 316-316, 331-331, 346-346, 363-363, 382-382, 388-388, 398-398, 415-415, 426-426, 438-438, 454-454, 554-554, 565-565, 583-583, 608-608, 617-617, 626-626, 637-637

packages/react-router/src/route.tsx (7)

15-15: LGTM! New imports for history state support.

The imports for useHistoryState and UseHistoryStateRoute types are correctly added and properly organized.

Also applies to: 47-47, 77-77


125-132: LGTM! History state hook implementation in RouteApi.

The useHistoryState implementation follows the same pattern as other route hooks like useSearch and useParams, ensuring consistency across the API.


281-288: LGTM! Consistent implementation of useHistoryState in Route class.

The implementation maintains consistency with other hook methods and correctly passes the route's ID as the from parameter.


391-419: LGTM! Root route context factory properly extended.

The createRootRouteWithContext function correctly includes TStateValidator in its generics and passes it through to createRootRoute, maintaining type safety across the root route creation flow.


505-512: LGTM! RootRoute's useHistoryState implementation.

The hook implementation in RootRoute follows the established pattern and correctly uses RootRouteId for the from parameter.


186-186: Verify TStateValidator propagation in all route helpers

Please confirm that every public route-creation function has been updated to include the new TStateValidator generic parameter. In particular:

  • In packages/react-router/src/route.tsx, inspect the signature of:
    • export function createRoute<…> (around line 318)
    • export const createRouter: CreateRouterFn<…> (in src/router.ts)
    • Any createRootRouteWithContext<…> overloads
  • In packages/react-router/src/fileRoute.ts, ensure its createRoute = <…> assignment includes TStateValidator
  • Repeat the same checks for createRouteMask, createRouteNodes…, and any adapter packages (e.g. valibot, zod, arktype, react-router-adapter, etc.)

We didn’t find conclusive evidence that these signatures were all updated—please double-check and add TStateValidator wherever missing.


611-667: Document NotFoundRoute state behavior.

The TanStack Router docs confirm that navigating to a NotFoundRoute treats history state like any other client-side navigation:

  • State passed via navigate('/404', { state: yourState }) is preserved in the history API.
  • NotFoundRoute does not implicitly clear or reset history.state.
  • On a hard reload or new tab, in-memory state won’t be restored (use URL/search params or storage for persistence).

Consider adding a brief JSDoc or code comment above NotFoundRoute in packages/react-router/src/route.tsx to capture these points.
[optional_refactors_recommended]

packages/solid-router/tests/Matches.test-d.tsx (1)

18-27: LGTM! Type tests correctly updated for state schema support.

All RouteMatch type aliases have been correctly updated to include the new fullStateSchema type parameter in the 5th position, maintaining consistency with the router-core type system changes. The runtime test logic remains unchanged.

Also applies to: 36-45, 53-62, 66-75, 84-93, 110-119, 134-143

packages/react-router/tests/Matches.test-d.tsx (1)

17-26: LGTM! React Router tests aligned with state schema changes.

The type tests correctly mirror the Solid Router changes, with all RouteMatch type aliases properly updated to include the fullStateSchema parameter. This ensures type consistency across both React and Solid implementations.

Also applies to: 35-44, 52-61, 65-74, 83-92, 109-118, 133-142

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

38-38: LGTM! Import for state validator resolution.

The import of ResolveStateValidatorInput is correctly added alongside the existing search validator imports.


52-54: LGTM! StateSchemaInput type token defined.

The StateSchemaInput type follows the same pattern as SearchSchemaInput and serves as a type token for state schema input resolution.


112-118: LGTM! State schema input inference helper.

The InferFullStateSchemaInput type helper correctly extracts the full state schema input from a route's types, following the established pattern.


176-181: LGTM! State schema resolution from validator.

The ResolveStateSchemaFn correctly extracts the state schema from a validator function, maintaining consistency with the search schema pattern.


182-198: LGTM! Full state schema composition logic.

The state schema composition correctly merges parent and child state schemas using IntersectAssign, mirroring the search schema composition pattern.


356-373: LGTM! State schema resolution and input composition.

The state validator resolution logic properly handles all validator types (standard schema, adapter, object, and function validators) consistently with the search validator pattern.


445-476: LGTM! RouteTypes interface extended with state typing.

The RouteTypes interface correctly includes all state-related type properties, maintaining parallel structure with search schema types.


894-894: LGTM! FileBaseRouteOptions includes state validation.

The validateState option is correctly added with proper type constraints, allowing routes to define state validation alongside search validation.


1076-1089: LGTM! Asset function contexts include state schema.

The AssetFnContextOptions correctly includes TStateValidator and properly resolves the full state schema in RouteMatch contexts, ensuring state information is available for asset generation functions.

Also applies to: 1105-1105


1185-1185: LGTM! Route lifecycle hooks receive state schema.

The onEnter, onStay, and onLeave hooks correctly receive RouteMatch instances that include the resolved full state schema, enabling state-aware lifecycle management.

Also applies to: 1202-1202, 1219-1219


1394-1395: LGTM! RouteConstraints extended with state schemas.

The RouteConstraints interface correctly includes both TStateSchema and TFullStateSchema, maintaining consistency with the search schema constraints.


785-801: AnyRoute parameter count consistent

All occurrences of AnyRoute in the codebase use the non-generic alias (which already applies all 15 parameters) and there are no references to AnyRoute<…> that need updating. No changes required.

packages/router-core/src/fileRoute.ts (3)

42-42: LGTM! FileRouteOptions properly extended with state validator.

The TStateValidator generic parameter is correctly added to FileRouteOptions and properly threaded through to both FileBaseRouteOptions and UpdatableRouteOptions base types.

Also applies to: 53-53, 67-67


83-83: LGTM! CreateFileRoute supports state validation.

The CreateFileRoute type correctly includes TStateValidator as a generic parameter with a default of undefined, and properly passes it through the FileRouteOptions and resulting Route type.

Also applies to: 97-97, 111-111


129-129: LGTM! LazyRouteOptions accommodates state validator.

The LazyRouteOptions type correctly includes a second AnyValidator placeholder for TStateValidator in the UpdatableRouteOptions generic parameters.

Copy link
Contributor

@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: 2

🧹 Nitpick comments (2)
packages/router-core/src/router.ts (2)

2433-2463: Treat “thenables” as async to avoid false negatives in non-native Promise impls

instanceof Promise misses custom thenables. Use a duck-typed then-check to reliably reject async validators.

Apply this diff:

   if ('~standard' in validator) {
     const result = validator['~standard'].validate(input)
-
-    if (result instanceof Promise)
-      throw new ErrorClass('Async validation not supported')
+    // Robust thenable detection (covers non-native Promise impls)
+    if (result && typeof (result as any).then === 'function') {
+      throw new ErrorClass('Async validation not supported')
+    }

2473-2479: Avoid duplicating componentTypes; re-export the canonical one

componentTypes is already defined in load-matches. Duplicating the constant here risks drift over time. Re-export the original instead.

Apply this diff:

-export const componentTypes = [
-  'component',
-  'errorComponent',
-  'pendingComponent',
-  'notFoundComponent',
-] as const
+// Re-export canonical definition to avoid duplication
+export { componentTypes } from './load-matches'
📜 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 120ba3e and 50a7dba.

📒 Files selected for processing (1)
  • packages/router-core/src/router.ts (8 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
packages/router-core/src/router.ts (4)
packages/history/src/index.ts (1)
  • omitInternalKeys (103-111)
packages/router-core/src/utils.ts (1)
  • replaceEqualDeep (222-269)
packages/router-core/src/validators.ts (1)
  • AnyValidator (69-69)
packages/router-core/src/load-matches.ts (1)
  • componentTypes (967-972)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Preview
  • GitHub Check: Test
🔇 Additional comments (6)
packages/router-core/src/router.ts (6)

5-7: Good call: sanitize state via omitInternalKeys

Importing omitInternalKeys is the right move to keep internal router keys out of user-facing validation/state.


1291-1294: Match object: structural sharing for state looks correct

replaceEqualDeep usage for state matches how search/params are handled. This should minimize unnecessary renders in adapters.


1321-1325: Initial match: state/_strictState wiring is consistent

Initializing state, _strictState, and stateError for new matches aligns with the search path. Looks good.


1358-1360: Propagating stateError onto matches is correct

This mirrors searchError behavior and keeps error handling uniform.


2391-2392: StateParamError public type looks good

Public error aligns with SearchParamError and enables consistent userland handling.


2465-2471: Nice unification of validation helpers

validateState/validateSearch wrappers around validateInput reduce duplication and centralize error-class mapping.

Copy link
Contributor

@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: 0

🧹 Nitpick comments (9)
packages/router-core/src/route.ts (9)

52-55: StateSchemaInput marker mirrors SearchSchemaInput — LGTM

This keeps the validator-input pattern symmetric with search. Consider a short JSDoc above this type to document the marker’s intent.


170-175: ResolveStateSchemaFn symmetry — OK

This mirrors ResolveSearchSchemaFn. See my later note about deduplicating the mapping logic to reuse ResolveValidatorOutput.


350-360: Unify state validator output mapping with ResolveValidatorOutput

ResolveStateSchema duplicates the same conditional mapping already available via ResolveValidatorOutput (used for search). Prefer reusing ResolveValidatorOutput for consistency and maintainability.

Apply:

-export type ResolveStateSchema<TStateValidator> =
-  unknown extends TStateValidator
-    ? TStateValidator
-    : TStateValidator extends AnyStandardSchemaValidator
-      ? NonNullable<TStateValidator['~standard']['types']>['output']
-      : TStateValidator extends AnyValidatorAdapter
-        ? TStateValidator['types']['output']
-        : TStateValidator extends AnyValidatorObj
-          ? ResolveStateSchemaFn<TStateValidator['parse']>
-          : ResolveStateSchemaFn<TStateValidator>
+export type ResolveStateSchema<TStateValidator> =
+  ResolveValidatorOutput<TStateValidator>

This keeps RouteTypes.stateSchema and ResolveFullStateSchema behavior identical while removing duplicate conditional logic.


432-444: RouteTypes: state schema fields added — check naming/doc parity with search

The new fields (stateSchema, stateSchemaInput, fullStateSchema, fullStateSchemaInput, stateValidator) mirror search fields — good. Consider brief JSDoc summaries mirroring search’s to make discoverability clear for users browsing types.

Minor consistency note: searchSchema uses ResolveValidatorOutput while stateSchema uses ResolveStateSchema (which in turn replicates the same mapping). If you accept the earlier refactor, these will be fully aligned.

Also applies to: 458-471


801-842: RouteOptions gained TStateValidator and carries it into UpdatableRouteOptions

Looks consistent. Verify any external public types in docs/sample code that explicitly mention the generic parameter list are updated.


873-889: FileBaseRouteOptions: validateState introduced — add minimal JSDoc

validateState mirrors validateSearch; suggest a short doc comment clarifying strict/non-strict semantics and that an empty object {} is validated on direct navigation if no state exists (to mirror the behavior explained in the PR discussion).

I can draft the JSDoc based on your example semantics if helpful.


1346-1379: RootRouteOptions and RootRoute/BaseRootRoute: state generics included

Parity with search generics for root-level routes is correct. Ensure external TypeDocs and migration notes mention the new generic slot for users doing explicit generic annotations on RootRoute/BaseRootRoute.


1405-1415: RouteMask gains state?: any — consider tightening later

Matches existing any-typed search. Optional future enhancement: make state typed when to is a specific RoutePaths using RouteById’s types. Non-blocking.


1051-1063: SSR context intentionally omits state — confirm design

Since history state is ephemeral and client-only, excluding it from SsrContextOptions makes sense. Just confirming this is intentional and documented.

📜 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 dd93cb1 and fe9d1dc.

📒 Files selected for processing (1)
  • packages/router-core/src/route.ts (62 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
packages/router-core/src/route.ts (2)
packages/router-core/src/validators.ts (6)
  • AnySchema (71-71)
  • AnyStandardSchemaValidator (12-12)
  • AnyValidatorAdapter (56-56)
  • AnyValidatorObj (46-46)
  • ResolveStateValidatorInput (100-101)
  • AnyValidator (69-69)
packages/router-core/src/utils.ts (3)
  • IntersectAssign (74-82)
  • NoInfer (5-5)
  • Constrain (131-133)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Test
  • GitHub Check: Preview
🔇 Additional comments (14)
packages/router-core/src/route.ts (14)

112-118: InferFullStateSchemaInput aligns with InferFullSearchSchemaInput

Consistent naming and behavior. This will simplify deriving input types for composed routes.


176-185: fullStateSchema composition logic matches search’s IntersectAssign

Parent-first intersection using IntersectAssign is correct and consistent with search schema composition (empty object short-circuit). Good.


186-193: InferFullStateSchema — LGTM

Consistent extraction of the composed state schema from route.types.


222-226: Deprecation notes preserved

The deprecation comments for parseParams/stringifyParams are intact and consistent with the newer params.{parse,stringify}.

Also applies to: 227-231


361-367: ResolveFullStateSchemaInput — LGTM

Correctly parallels ResolveFullSearchSchemaInput using ResolveStateValidatorInput and IntersectAssign.


611-647: Route interface: propagated TStateValidator and types — LGTM

State generics and types are threaded through Route correctly. This unblocks RouteTypesById-based introspection of fullStateSchema for downstream consumers.


779-795: AnyRoute updated arity — LGTM

Arity matches Route’s parameter count after adding TStateValidator.


973-1002: BaseRouteOptions generic list updated — LGTM

Propagation into BaseRouteOptions and retention of defaults look correct.


1064-1111: AssetFnContextOptions and RouteMatch contexts now include fullStateSchema

Good propagation into headers/head/scripts contexts and general asset functions. This keeps parity with search and makes state visible across lifecycle hooks.


1123-1283: UpdatableRouteOptions and lifecycle hooks: state wired through — verify deprecation/legacy fields

  • onEnter/onStay/onLeave now receive RouteMatch with fullStateSchema — good.
  • No state middlewares are introduced (by design, since state isn’t URL-addressable) — fine.
  • preSearchFilters/postSearchFilters retained as deprecated; no state analog needed.

No requested changes here.


1435-1468: BaseRoute generics and methods: TStateValidator flows through types, update(updateLoader/lazy) signatures preserve it

All factory/update/lazy helpers preserve the added generic. Looks consistent across methods. Nice job keeping the return types precise.

Also applies to: 1496-1516, 1522-1537, 1547-1563, 1643-1658, 1662-1677, 1689-1704, 1724-1739, 1742-1756, 1761-1778


1797-1823: RootRoute/BaseRootRoute interfaces/classes: state generics plumbed correctly

End-to-end propagation looks consistent. No issues spotted.

Also applies to: 1825-1865


31-44: No ESM cycle risk – validators.ts correctly uses a type‐only import
The grep output shows

import type { SearchSchemaInput, StateSchemaInput } from './route'

and no non-type imports from route.ts in validators.ts. This confirms that both sides use import type and there is no runtime ESM cycle.


504-540: No explicit generic instantiations detected—safe to proceed

I searched the entire packages/ folder for any occurrences of RouteAddChildrenFn<, RouteAddFileChildrenFn<, or RouteAddFileTypesFn< that weren’t just the type-alias definitions or internal property typings. The only hits were:

  • The type-alias declarations themselves in packages/router-core/src/route.ts (lines 504–608)
  • The server-route equivalent in packages/start-server-core/src/serverRoute.ts (lines 204–208, 320–324)
  • Internal assignments of those aliases to the _add*/addChildren properties in those same files

There are no calls or docs examples supplying explicit generic arguments for these functions elsewhere. Since consumers rely on inference, adding the new TStateValidator parameter will not break any manually specified generics.

Copy link
Contributor

@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: 0

Caution

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

⚠️ Outside diff range comments (1)
packages/router-core/src/router.ts (1)

1824-1831: Dead path: _includeValidateState is never enabled during navigations

buildAndCommitLocation only enables _includeValidateSearch. As a result, the buildLocation state-validation branch never executes during navigations.

Set _includeValidateState: true alongside search.

-    const location = this.buildLocation({
-      ...(rest as any),
-      _includeValidateSearch: true,
-    })
+    const location = this.buildLocation({
+      ...(rest as any),
+      _includeValidateSearch: true,
+      _includeValidateState: true,
+    })
♻️ Duplicate comments (2)
packages/router-core/src/router.ts (2)

1609-1638: Propagation gap: validateState path is gated but not exercised across call sites

Even after fixing the sanitizer, the validators won’t run unless _includeValidateState is passed in from framework integrations (navigate, Link, Transitioner, RouterProvider, etc.). Please propagate this flag similarly to _includeValidateSearch.

You can verify all call sites quickly:

#!/bin/bash
# Show callers of buildLocation and uses of _includeValidateState
rg -n --hidden --glob '!**/dist/**' '\bbuildLocation\s*\(' -C2
rg -n --hidden --glob '!**/dist/**' '_includeValidateState' -C3

# Inspect framework integrations that compose navigation options
rg -n --hidden --glob '!**/dist/**' '\bnavigate\s*\(' -C3
rg -n --hidden --glob '!**/dist/**' '<Link\b|useNavigate|RouterProvider' -n -C3 --type ts --type tsx

1620-1638: Bug: validators see internal keys and internal keys are dropped from committed state in buildLocation

In the buildLocation path, validators are fed nextState (including internal keys like _TSR* and key), and after validation you replace the state entirely with validatedState, losing those internal keys. This can cause validator failures and break navigation semantics.

  • Sanitize inputs with omitInternalKeys before validation.
  • After validation, re-attach internal keys to the final state.
  • Optionally reapply replaceEqualDeep to preserve structural sharing.

Apply this diff:

       // Replace the equal deep
       nextState = replaceEqualDeep(currentLocation.state, nextState)

-      if (opts._includeValidateState) {
-        let validatedState = {}
-        destRoutes.forEach((route) => {
-          try {
-            if (route.options.validateState) {
-              validatedState = {
-                ...validatedState,
-                ...(validateState(route.options.validateState, {
-                  ...validatedState,
-                  ...nextState,
-                }) ?? {}),
-              }
-            }
-          } catch {
-            // ignore errors here because they are already handled in matchRoutes
-          }
-        })
-        nextState = validatedState
-      }
+      if (opts._includeValidateState) {
+        // 1) Do not feed internal keys to validators
+        const sanitizedNextState = nextState
+          ? omitInternalKeys(nextState as any)
+          : {}
+        // 2) Validate through all matching route validators
+        let validatedState = {}
+        destRoutes.forEach((route) => {
+          try {
+            if (route.options.validateState) {
+              validatedState = {
+                ...validatedState,
+                ...(validateState(route.options.validateState, {
+                  ...validatedState,
+                  ...sanitizedNextState,
+                }) ?? {}),
+              }
+            }
+          } catch {
+            // ignore errors here because they are already handled in matchRoutes
+          }
+        })
+        // 3) Preserve internal keys like __TSR_* and 'key'
+        const internalOnly = Object.fromEntries(
+          Object.entries(nextState as any).filter(
+            ([k]) => k.startsWith('__') || k === 'key',
+          ),
+        )
+        nextState = replaceEqualDeep(
+          currentLocation.state,
+          { ...internalOnly, ...validatedState } as any,
+        )
+      }
🧹 Nitpick comments (6)
packages/router-core/src/router.ts (6)

1201-1230: State validation in matchRoutes: parity with search is close; wrap errors and use nullish coalescing

  • You already filter internal keys before validation and always flow through the helper — nice.
  • For parity with search, wrap thrown errors into StateParamError when they aren’t already that type.
  • Prefer “?? {}” over “|| {}” to avoid accidentally treating legitimate falsey values as empty, even though validators should return objects.

Apply this focused diff:

-          const strictState =
-            validateState(route.options.validateState, filteredState) || {}
+          const strictState =
+            validateState(route.options.validateState, filteredState) ?? {}
           return [
             {
               ...filteredState,
               ...strictState,
             },
             { ...parentStrictState, ...strictState },
             undefined,
           ]
-        } catch (err: any) {
-          const stateValidationError = err
-
-          if (opts?.throwOnError) {
-            throw stateValidationError
-          }
-
-          return [filteredState, {}, stateValidationError]
+        } catch (err: any) {
+          let stateParamError = err
+          if (!(err instanceof StateParamError)) {
+            stateParamError = new StateParamError(
+              err?.message ?? 'Invalid state',
+              { cause: err },
+            )
+          }
+          if (opts?.throwOnError) {
+            throw stateParamError
+          }
+          return [filteredState, {}, stateParamError]
         }

1877-1884: SSR parity: enable state validation when normalizing the initial location

beforeLoad normalizes the initial SSR location with _includeValidateSearch only. For parity and early error surfacing, also pass _includeValidateState.

       const nextLocation = this.buildLocation({
         to: this.latestLocation.pathname,
         search: true,
         params: true,
         hash: true,
         state: true,
-        _includeValidateSearch: true,
+        _includeValidateSearch: true,
+        _includeValidateState: true,
       })

2430-2460: Validation helper: clarify async behavior and broaden ergonomics

validateInput throws “Async validation not supported” when a standard validator returns a Promise. If async may surface via community validators, consider either (a) documenting this clearly in the API, or (b) supporting async and plumbing it through match/build paths.

Also, for the “parse” and “function” branches, any thrown non-State/SearchParamError bubbles up raw; you already wrap search errors at call sites — mirror that for state (see earlier comment).

Do we guarantee that route-level validators will always be synchronous across integrations (Zod/Valibot/ArkType/custom)? If not, I can propose a safe async-capable path.


2470-2476: Avoid duplicate sources of truth for componentTypes; re-export instead

componentTypes is already exported from load-matches. Duplicating it here risks drift. Prefer re-exporting.

-export const componentTypes = [
-  'component',
-  'errorComponent',
-  'pendingComponent',
-  'notFoundComponent',
-] as const
+export { componentTypes } from './load-matches'

1610-1619: Nit: replaceEqualDeep before validation is fine; consider reusing it after re-attaching internals

If you adopt the buildLocation fix, ensure the final merged state also goes through replaceEqualDeep with currentLocation.state to keep structural sharing predictable for selectors.


1206-1209: Type-safety nit: rawState cast with omitInternalKeys

If rawState’s type is wider than Record<string, unknown>, TypeScript may complain. Cast to any to match omitInternalKeys’ signature as you did elsewhere in the codebase.

-        const filteredState = rawState ? omitInternalKeys(rawState) : {}
+        const filteredState = rawState ? omitInternalKeys(rawState 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 fe9d1dc and 16825f3.

📒 Files selected for processing (1)
  • packages/router-core/src/router.ts (8 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
packages/router-core/src/router.ts (4)
packages/history/src/index.ts (1)
  • omitInternalKeys (103-111)
packages/router-core/src/utils.ts (1)
  • replaceEqualDeep (222-269)
packages/router-core/src/validators.ts (1)
  • AnyValidator (69-69)
packages/router-core/src/load-matches.ts (1)
  • componentTypes (967-972)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Test
  • GitHub Check: Preview
🔇 Additional comments (3)
packages/router-core/src/router.ts (3)

5-7: LGTM: importing omitInternalKeys from @tanstack/history

Good move centralizing the internal-keys filter in history and reusing it here. See my comment below about also using it in buildLocation’s state-validation path.


1287-1291: LGTM: state/_strictState plumbed into matches + stateError surfaced

The wiring of state, _strictState, and stateError mirrors search semantics and fits the match lifecycle cleanly.

Also applies to: 1317-1322, 1356-1357


2388-2389: LGTM: dedicated StateParamError

Explicit error type for state validation aligns with SearchParamError and improves downstream handling.

@naoya7076
Copy link
Contributor Author

@schiller-manuel

Thank you for your daily development work on TanStack Router! I'm also looking forward to the development of TanStack Start.

It's been over a month since the previous comment. I've also addressed the CodeRabbit comments, so please review when you have a chance.

Copy link
Contributor

@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: 0

🧹 Nitpick comments (3)
packages/router-core/src/Matches.ts (3)

124-125: Adding TFullStateSchema mid-arity is a public type break; consider a legacy alias

Consumers using RouteMatch with 7 type args will now error. Mitigate via a back-compat alias and call it out in release notes.

Add a legacy alias (placement near existing exports is fine):

+// Back-compat for pre-state RouteMatch arity
+export type RouteMatchLegacy7<
+  TRouteId,
+  TFullPath,
+  TAllParams,
+  TFullSearchSchema,
+  TLoaderData,
+  TAllContext,
+  TLoaderDeps
+> = RouteMatch<
+  TRouteId,
+  TFullPath,
+  TAllParams,
+  TFullSearchSchema,
+  unknown,
+  TLoaderData,
+  TAllContext,
+  TLoaderDeps
+>

163-165: state/_strictState mirror search — add quick JSDoc for clarity

Tiny DX boost: document that these mirror search vs. _strictSearch semantics.

   search: TFullSearchSchema
   _strictSearch: TFullSearchSchema
-  state: TFullStateSchema
-  _strictState: TFullStateSchema
+  /** Validated history state for this match (mirrors `search`). */
+  state: TFullStateSchema
+  /** Route-strict view of state (mirrors `_strictSearch`). */
+  _strictState: TFullStateSchema

260-261: Union maker includes state — LGTM; consider pre-validation parity

Nice propagation. For full parity with params/search, consider adding state to PreValidationErrorHandlingRouteMatch.

Proposed shape:

-export interface PreValidationErrorHandlingRouteMatch<
-  TRouteId,
-  TFullPath,
-  TAllParams,
-  TFullSearchSchema,
-> {
+export interface PreValidationErrorHandlingRouteMatch<
+  TRouteId,
+  TFullPath,
+  TAllParams,
+  TFullSearchSchema,
+  TFullStateSchema,
+> {
   ...
   search:
     | { status: 'success'; value: TFullSearchSchema }
     | { status: 'error'; error: unknown }
+  state:
+    | { status: 'success'; value: TFullStateSchema }
+    | { status: 'error'; error: unknown }
   ...
 }
 
-export type MakePreValidationErrorHandlingRouteMatchUnion<...> =
-  PreValidationErrorHandlingRouteMatch<..., TRoute['types']['fullSearchSchema']>
+export type MakePreValidationErrorHandlingRouteMatchUnion<...> =
+  PreValidationErrorHandlingRouteMatch<..., TRoute['types']['fullSearchSchema'], TRoute['types']['fullStateSchema']>

Happy to push a patch if desired.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ee1139f and 6af18d5.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (3)
  • packages/router-core/src/Matches.ts (8 hunks)
  • packages/router-core/src/router.ts (9 hunks)
  • packages/router-devtools-core/package.json (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/router-devtools-core/package.json
  • packages/router-core/src/router.ts
🧰 Additional context used
🧬 Code graph analysis (1)
packages/router-core/src/Matches.ts (1)
packages/router-core/src/routeInfo.ts (1)
  • FullStateSchema (222-225)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Test
  • GitHub Check: Preview
🔇 Additional comments (5)
packages/router-core/src/Matches.ts (5)

7-7: Type-only import for FullStateSchema — LGTM; check for cycles

Good use of type-only import. Please run a quick build to ensure no circular type deps between Matches.ts and routeInfo.ts.


141-141: stateError parity — ensure it’s surfaced wherever searchError is

Wire this into the same error flows/devtools surfaces as searchError (notFound/error boundaries, panels).


218-219: Propagate fullStateSchema in MakeRouteMatchFromRoute — LGTM


237-240: TStrict mapping for state matches search — LGTM

Non-strict uses FullStateSchema; strict uses route-local schema. Consistent with search.


249-249: No internal 7-arity RouteMatch usages found — a repo-wide search returned zero instances of RouteMatch with seven type arguments; proceed with the 8-arity update but confirm any external consumers are compatible or add a legacy alias if backward-compatibility is required.

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.

4 participants