router icon indicating copy to clipboard operation
router copied to clipboard

feat(router): Add useHistoryState hook for type-safe state management

Open naoya7076 opened this issue 9 months ago โ€ข 4 comments

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:

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

For example:

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

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

Key features of useHistoryState

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

Example usage

Users can define state validation on routes:

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

Then use it in components:

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

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

naoya7076 avatar Apr 09 '25 14:04 naoya7076

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.

schiller-manuel avatar May 08 '25 17:05 schiller-manuel

any update for this PR? I'm waiting for this feature to be merged soon

hleekeeper avatar Jun 04 '25 19:06 hleekeeper

View your CI Pipeline Execution โ†— for commit 46dcebd17c06ab0fbbcbd9d324da7266f8eeab25


โ˜๏ธ Nx Cloud last updated this comment at 2025-09-27 06:41:43 UTC

nx-cloud[bot] avatar Jun 14 '25 19:06 nx-cloud[bot]

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 avatar Jun 14 '25 19:06 schiller-manuel

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

naoya7076 avatar Jun 22 '25 15:06 naoya7076

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 avatar Jun 23 '25 15:06 hleekeeper

@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

naoya7076 avatar Jun 24 '25 05:06 naoya7076

@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 avatar Jun 24 '25 15:06 hleekeeper

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

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

This resolves to:

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

naoya7076 avatar Jun 25 '25 00:06 naoya7076

Hi @schiller-manuel,

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

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

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

Thanks!

naoya7076 avatar Jul 10 '25 14:07 naoya7076

sorry for being slow to review. will do so soon

schiller-manuel avatar Jul 10 '25 22:07 schiller-manuel

More templates

@tanstack/arktype-adapter

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

commit: 6af18d5

pkg-pr-new[bot] avatar Jul 11 '25 13:07 pkg-pr-new[bot]

any update on the PR's review?

hleekeeper avatar Aug 04 '25 15:08 hleekeeper

Walkthrough

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

Changes

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

Sequence Diagram(s)

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

  UI->>Hook: call useHistoryState({ from, strict, shouldThrow, select })
  Hook->>Match: useMatch({ from, strict, shouldThrow, select })
  Match->>Core: resolve matches for `from`
  Core->>Route: build/lookup match (include preMatchState)
  Core->>Route: if validateState -> sanitize (omitInternalKeys) -> validate -> merge _strictState / stateError
  Note right of Route: match now includes state, _strictState, stateError
  Match-->>Hook: match (filtered state)
  Hook-->>UI: selected value or throw/undefined
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.

coderabbitai[bot] avatar Aug 16 '25 23:08 coderabbitai[bot]

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

naoya7076 avatar Aug 24 '25 13:08 naoya7076