feat(router): Add useHistoryState hook for type-safe state management
Proposal
- https://github.com/TanStack/router/discussions/4113
Overview
This PR introduces the new useHistoryState hook and related functionality to enable type-safe history state management in TanStack Router.
- https://github.com/TanStack/router/discussions/975#discussioncomment-10414075
- https://github.com/TanStack/router/discussions/284#discussioncomment-9344482
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:
- Type pollution - types appear in places where they aren't needed
- 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;
}
}
https://github.com/TanStack/router/discussions/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
useHistoryStatehook export in the main package index - Extended the
FileRouteclass to support state validation withTStateValidatorgeneric parameter - Created a full example in
examples/react/basic-history-statedemonstrating 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
})
thanks for your PR. we are currently focusing on finalizing our devinxi efforts. as soon as this branch is merged we'll revisit this PR.
any update for this PR? I'm waiting for this feature to be merged soon
View your CI Pipeline Execution โ for commit 46dcebd17c06ab0fbbcbd9d324da7266f8eeab25
โ๏ธ Nx Cloud last updated this comment at 2025-09-27 06:41:43 UTC
can you please add type tests, similar to what we do for search?
also, what happens if I open up /posts directly and it has a state validator, but no state is present? does this mean all state needs to be optional?
@schiller-manuel
> can you please add type tests, similar to what we do for search?
added tests 481df10 (#3967) and 5315139 (#3967)
> what happens if I open up /posts directly and it has a state validator, but no state is present?
This behavior is same as how useSearch works.
Current Behavior
When you navigate directly to a /posts with a state validator but no state present:
- The validator receives an empty object
{} - Validation outcome depends on your validator implementation
Example
validateState: (input) =>
z.object({
example: z.string().default('default-example'),
count: z.number().default(0),
options: z.array(z.string()).default([]),
}).parse(input),
Type System Handles This by strict
-
strict: trueโ required properties ({ page: number }) -
strict: falseโ optional properties ({ page?: number })
When strict: false, properties become optional.
Answer to Your Question
Does this mean all state needs to be optional?
No.You have the same options as with search params.
Hi @naoya7076, thanks for putting this up together.
I have a question. How can one access to the route-specific history state type? For example, currently to access the type of history state, the HistoryState type is used. But this is just an aggregation of all possible history states. But let say I want to find the history state type of a specific path /example, would it be possible to do something like HistroyState['/example']?
@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
@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!
@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[];
};
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
validateStateandvalidateSearch - 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!
sorry for being slow to review. will do so soon
More templates
- tanstack-router-react-example-authenticated-routes
- tanstack-router-react-example-authenticated-routes-firebase
- tanstack-router-react-example-basic
- tanstack-router-react-example-basic-default-search-params
- tanstack-router-react-example-basic-devtools-panel
- tanstack-router-react-example-basic-file-based
- tanstack-router-react-example-basic-history-state
- tanstack-router-react-example-basic-non-nested-devtools
- tanstack-router-react-example-react-query
- tanstack-router-react-example-basic-react-query-file-based
- tanstack-router-react-example-basic-ssr-file-based
- tanstack-router-react-example-basic-ssr-streaming-file-based
- tanstack-router-react-example-basic-virtual-file-based
- tanstack-router-react-example-basic-virtual-inside-file-based
- tanstack-router-react-example-deferred-data
- tanstack-router-react-example-kitchen-sink
- tanstack-router-react-example-kitchen-sink-file-based
- tanstack-router-react-example-kitchen-sink-react-query
- tanstack-router-react-example-kitchen-sink-react-query-file-based
- tanstack-router-react-example-large-file-based
- tanstack-router-react-example-location-masking
- tanstack-router-react-example-navigation-blocking
- tanstack-router-react-example-quickstart
- tanstack-router-react-example-quickstart-esbuild-file-based
- tanstack-router-react-example-quickstart-file-based
- tanstack-router-react-example-quickstart-rspack-file-based
- tanstack-router-react-example-quickstart-webpack-file-based
- router-monorepo-react-query
- router-mono-simple
- router-mono-simple-lazy
- tanstack-router-react-example-scroll-restoration
- tanstack-search-validator-adapters
- tanstack-start-example-bare
- tanstack-start-example-basic
- tanstack-start-example-basic-auth
- tanstack-start-example-basic-react-query
- tanstack-start-example-basic-rsc
- tanstack-start-example-basic-static
- tanstack-start-example-clerk-basic
- tanstack-start-example-convex-trellaux
- tanstack-start-example-counter
- tanstack-start-example-large
- tanstack-start-example-material-ui
- tanstack-start-example-supabase-basic
- tanstack-start-tailwind-v4
- tanstack-start-example-trellaux
- tanstack-start-example-workos
- tanstack-router-react-example-view-transitions
- tanstack-router-react-example-with-framer-motion
- tanstack-router-react-example-with-trpc
- tanstack-router-react-example-with-trpc-react-query
- tanstack-router-solid-example-basic
- tanstack-router-solid-example-basic-devtools-panel
- tanstack-router-solid-example-basic-file-based
- tanstack-router-solid-example-basic-non-nested-devtools
- tanstack-router-solid-example-basic-solid-query
- tanstack-router-solid-example-basic-solid-query-file-based
- tanstack-router-solid-example-basic-ssr-streaming-file-based
- tanstack-router-solid-example-kitchen-sink-file-based
- tanstack-router-solid-example-quickstart-file-based
- tanstack-solid-start-example-bare
- tanstack-solid-start-example-basic
- tanstack-solid-start-example-basic-static
@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
any update on the PR's review?
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 |
|---|---|
Documentationdocs/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 utilitypackages/history/src/index.ts |
Adds omitInternalKeys(state) to remove internal history keys (keys starting with __ and key). |
React Router: hooks & APIspackages/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 & validationpackages/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 & APIspackages/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. |
Devtoolspackages/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 testspackages/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 testspackages/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 & stylingexamples/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
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
- TanStack/router#4978 โ touches router-core build/location/from-path resolution; overlaps code paths modified for state validation and matching logic.
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.
@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.