-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
feat(router): Add useHistoryState hook for type-safe state management #3967
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
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.
…tools panel This commit introduces a new function to filter out internal properties (keys starting with '__') from the router state before it is displayed in the BaseTanStackRouterDevtoolsPanel.
…nction in useHistoryState
…n useHistoryState
…router Router class
…TanStackRouterDevtoolsPanel
…toolsPanel by merging strict state
I guess we can access the route-specific history state type by |
@hleekeeper type DestinationRouteStateSchema = RouteById<
(typeof router)['routeTree'],
'/state-examples/destination'
>['types']['fullStateSchema']; This resolves to:
|
Hi @schiller-manuel, About 3 weeks ago, I have addressed all the feedback and replied your comments. All the requested changes have been implemented:
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! |
sorry for being slow to review. will do so soon |
More templates
@tanstack/arktype-adapter
@tanstack/directive-functions-plugin
@tanstack/eslint-plugin-router
@tanstack/history
@tanstack/react-router
@tanstack/react-router-devtools
@tanstack/react-router-ssr-query
@tanstack/react-start
@tanstack/react-start-client
@tanstack/react-start-plugin
@tanstack/react-start-server
@tanstack/router-cli
@tanstack/router-core
@tanstack/router-devtools
@tanstack/router-devtools-core
@tanstack/router-generator
@tanstack/router-plugin
@tanstack/router-ssr-query-core
@tanstack/router-utils
@tanstack/router-vite-plugin
@tanstack/server-functions-plugin
@tanstack/solid-router
@tanstack/solid-router-devtools
@tanstack/solid-start
@tanstack/solid-start-client
@tanstack/solid-start-plugin
@tanstack/solid-start-server
@tanstack/start-client-core
@tanstack/start-plugin-core
@tanstack/start-server-core
@tanstack/start-server-functions-client
@tanstack/start-server-functions-fetcher
@tanstack/start-server-functions-server
@tanstack/start-storage-context
@tanstack/valibot-adapter
@tanstack/virtual-file-routes
@tanstack/zod-adapter
commit: |
any update on the PR's review? |
WalkthroughAdds 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
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
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
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
Suggested reviewers
Poem
✨ Finishing Touches
🧪 Generate unit tests
Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
🔭 Outside diff range comments (1)
packages/solid-router/src/fileRoute.ts (1)
174-184
: Add missinguseHistoryState
to LazyRoute interface augmentationThe
LazyRoute
interface declared in the module augmentation for@tanstack/router-core
is out of sync with the class API—it exposesuseHistoryState
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 stateThis 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 semanticsFiltering internal keys via
omitInternalKeys
, merging strict results, and capturingstateError
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 ofomitInternalKeys
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 scriptThe example’s
package.json
defines adev
script (and also a genericstart
), so let’s standardize ondev
and addpnpm
: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 optionalThis 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 committedIf 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 DXToday 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 accessingimport.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 validatorsWhen 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 expectTInferredInput
(orResolveValidatorInputFn
) 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 : AnySchemaPlease 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 exampleClearer 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 colorThis 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 strategyYour 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 cachedMatchesgetMergedStrictState 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 redundantvalidatedStateValue 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 mismatchedThe 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 definingpostsRoute
before its component uses it
PostsComponent
closes overpostsRoute
which is declared later. It works at runtime because the component is invoked after assignment, but reordering to declarepostsRoute
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 stateConsider 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/hydrationYou added gated validation for state (
opts._includeValidateState
) similar to search, but callers don’t pass it. For parity withsearch
, consider propagating_includeValidateState: true
in:
buildAndCommitLocation
(alongside_includeValidateSearch: true
)- Server
beforeLoad
preflightbuildLocation
callThis 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 doubleas 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 acrossRouteApi
,Route
, andRootRoute
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
andRootRoute
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.
⛔ 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
: LGTMStandard PostCSS/Tailwind setup using ESM config. No issues.
examples/react/basic-history-state/.vscode/settings.json (1)
1-11
: LGTMExcluding 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 shapeThe function correctly strips both legacy
key
and internal__*
keys, matching howParsedHistoryState
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 inclusionThe new
_includeValidateState?: boolean
mirrors_includeValidateSearch
and unblocks the router’sbuildLocation
path to compose validated state. Confirmed by related handling inrouter.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 configThis 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 correctAdding @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 existingSearchSchemaInput
export.
110-111
: LGTM! State validator types properly exposed.The exports of
ResolveStateValidatorInput
andResolveStateValidatorInputFn
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
andFullStateSchemaInput
type aliases correctly follow the same pattern as the existingFullSearchSchema
andFullSearchSchemaInput
types. They properly useParseRoute
to extract route information andPartialMergeAll
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 asValidateSearch
andValidateParams
, correctly using the inference types (InferFrom
,InferStrict
,InferSelected
) withUseHistoryStateResult
.
193-201
: LGTM! Result validation type follows established patterns.The
ValidateUseHistoryStateResult
type mirrors the existingValidateUseSearchResult
andValidateUseParamsResult
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
andResolveStateValidatorInputFn
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-documentedfrom
option.The description clearly explains the optional route ID parameter and its fallback behavior.
18-24
: Comprehensivestrict
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
: ClearshouldThrow
option explanation.The documentation properly explains the error handling behavior and the alternative undefined return value.
32-37
: Excellentselect
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 sanitizationBringing 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 twoundefined
entries inBaseTanStackRouterDevtoolsPanel.tsx
correspond to the newTSearchValidator
andTStateValidator
slots inpackages/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 alignmentAdding 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 — goodDelegating 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 surfaceGood to see StateSchemaInput made part of the public types. Matches usage in tests.
343-349
: State validator input type exports are completeResolveStateValidatorInput(Fn) exports will be useful for adapter authors and advanced type-level workflows.
359-363
: New history-state type exports wired correctlyExporting UseHistoryStateResult and ResolveUseHistoryState from core makes framework hooks strongly typed.
404-426
: ValidateHistoryState and ValidateUseHistoryStateResult added — aligns with typePrimitivesPublicly exposing these maintains parity with existing ValidateSearch/ValidateUseSearchResult types.
packages/router-core/src/useHistoryState.ts (2)
5-13
: UseHistoryStateResult conditional on TSelected is correctThe “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 onResolveUseHistoryState
The core type must continue to accept
TFrom = undefined
(the default “closest‐match” case). IntroducingTFrom extends string ? … : neverwould make
ResolveUseHistoryState<Router, undefined, true>
resolve tonever
, breaking the default hook signature. Framework hooks already constrainTFrom
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: LGTMThe conditional typing for
state
based onResolveRelativePath
andRoutePaths/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 solidGood 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: LGTMClear
zod
-based validator and downstream consumption viaroute.useHistoryState()
. This is a great minimal example.packages/router-core/src/router.ts (1)
2433-2471
: Shared validation helper: Nice consolidationThe
validateInput
helper and thin wrappers forvalidateState
/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 theuseHistoryState
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 theFileRoute.createRoute
method signature and properly threaded through all dependent type definitions, maintaining consistency with the existingTSearchValidator
pattern.Also applies to: 95-95, 109-109, 123-123
224-231
: LGTM! Clean implementation ofuseHistoryState
onLazyRoute
.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 theRouteMatch
interface and consistently propagated through all related type constructors (MakeRouteMatchFromRoute
,MakeRouteMatch
,MakeRouteMatchUnion
). The implementation mirrors the existing pattern forTFullSearchSchema
.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 failureThe router’s state validation block returns the caught error as
stateError
(router.ts lines 1184–1203) and assigns it tomatch.stateError
(router.ts lines 1357–1359), matching theunknown
-typed field in Matches.ts.packages/solid-router/src/route.tsx (2)
15-15
: LGTM! Clean integration ofuseHistoryState
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
andUseHistoryStateRoute
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 likeuseSearch
anduseParams
, 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 tocreateRootRoute
, 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 thefrom
parameter.
186-186
: Verify TStateValidator propagation in all route helpersPlease 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 includesTStateValidator
- 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
inpackages/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 asSearchSchemaInput
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 consistentAll 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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 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 implsinstanceof 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 onecomponentTypes 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.
📒 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 omitInternalKeysImporting 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 correctreplaceEqualDeep usage for state matches how search/params are handled. This should minimize unnecessary renders in adapters.
1321-1325
: Initial match: state/_strictState wiring is consistentInitializing state, _strictState, and stateError for new matches aligns with the search path. Looks good.
1358-1360
: Propagating stateError onto matches is correctThis mirrors searchError behavior and keeps error handling uniform.
2391-2392
: StateParamError public type looks goodPublic error aligns with SearchParamError and enables consistent userland handling.
2465-2471
: Nice unification of validation helpersvalidateState/validateSearch wrappers around validateInput reduce duplication and centralize error-class mapping.
Remove try-catch around hook call and use router's default error boundary
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (9)
packages/router-core/src/route.ts (9)
52-55
: StateSchemaInput marker mirrors SearchSchemaInput — LGTMThis 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 — OKThis mirrors ResolveSearchSchemaFn. See my later note about deduplicating the mapping logic to reuse ResolveValidatorOutput.
350-360
: Unify state validator output mapping with ResolveValidatorOutputResolveStateSchema 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 searchThe 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 UpdatableRouteOptionsLooks 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 JSDocvalidateState 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 includedParity 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 laterMatches 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 designSince 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.
📒 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 InferFullSearchSchemaInputConsistent naming and behavior. This will simplify deriving input types for composed routes.
176-185
: fullStateSchema composition logic matches search’s IntersectAssignParent-first intersection using IntersectAssign is correct and consistent with search schema composition (empty object short-circuit). Good.
186-193
: InferFullStateSchema — LGTMConsistent extraction of the composed state schema from route.types.
222-226
: Deprecation notes preservedThe deprecation comments for parseParams/stringifyParams are intact and consistent with the newer params.{parse,stringify}.
Also applies to: 227-231
361-367
: ResolveFullStateSchemaInput — LGTMCorrectly parallels ResolveFullSearchSchemaInput using ResolveStateValidatorInput and IntersectAssign.
611-647
: Route interface: propagated TStateValidator and types — LGTMState generics and types are threaded through Route correctly. This unblocks RouteTypesById-based introspection of fullStateSchema for downstream consumers.
779-795
: AnyRoute updated arity — LGTMArity matches Route’s parameter count after adding TStateValidator.
973-1002
: BaseRouteOptions generic list updated — LGTMPropagation into BaseRouteOptions and retention of defaults look correct.
1064-1111
: AssetFnContextOptions and RouteMatch contexts now include fullStateSchemaGood 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 itAll 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 correctlyEnd-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 showsimport type { SearchSchemaInput, StateSchemaInput } from './route'and no non-type imports from
route.ts
invalidators.ts
. This confirms that both sides useimport type
and there is no runtime ESM cycle.
504-540
: No explicit generic instantiations detected—safe to proceedI searched the entire
packages/
folder for any occurrences ofRouteAddChildrenFn<
,RouteAddFileChildrenFn<
, orRouteAddFileTypesFn<
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 filesThere 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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 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 navigationsbuildAndCommitLocation 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 sitesEven 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 buildLocationIn 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 locationbeforeLoad 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 ergonomicsvalidateInput 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 insteadcomponentTypes 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 internalsIf 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 omitInternalKeysIf 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.
📒 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/historyGood 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 surfacedThe 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 StateParamErrorExplicit error type for state validation aligns with SearchParamError and improves downstream handling.
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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 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 aliasConsumers 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 clarityTiny 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 parityNice 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
⛔ 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 cyclesGood 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 isWire 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 — LGTMNon-strict uses FullStateSchema; strict uses route-local schema. Consistent with search.
249-249
: No internal 7-arityRouteMatch
usages found — a repo-wide search returned zero instances ofRouteMatch
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.
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:
For example:
#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:
This PR addresses these issues by adding route-specific, type-safe history state management.
Changes
useHistoryState
hook export in the main package indexFileRoute
class to support state validation withTStateValidator
generic parameterexamples/react/basic-history-state
demonstrating usage patternsKey features of useHistoryState
Example usage
Users can define state validation on routes:
Then use it in components:
Summary by CodeRabbit
New Features
Devtools
Documentation
Examples
Tests