Skip to content

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

Open
wants to merge 60 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
c32a680
feat(router): add validateState function for state validation in router
naoya7076 Mar 21, 2025
6008e61
feat(router): add TStateValidator type for state validation in routing
naoya7076 Mar 24, 2025
5249d78
add TStateValidator to route type definition
naoya7076 Mar 26, 2025
9ffaa51
feat(router): add useHistoryState for enhanced state management
naoya7076 Mar 29, 2025
1ea838c
feat(router): add state params display in devtools panel
naoya7076 Mar 29, 2025
62b2090
feat(router): implement useHistoryState hook for custom state management
naoya7076 Mar 29, 2025
79ef275
feat(router): add UseHistoryState types for enhanced state management
naoya7076 Mar 29, 2025
cf7ac4d
refactor(router): delete unused type
naoya7076 Mar 31, 2025
8ed329b
feat(router): enhance useHistoryState with additional options and imp…
naoya7076 Mar 31, 2025
96af77e
refactor(router): update useHistoryState.ts types for improved state …
naoya7076 Apr 6, 2025
4f4bc4c
refactor(router): replace useRouterState with useLocation in useHisto…
naoya7076 Apr 6, 2025
b3b4d8a
add useHistoryState basic example
naoya7076 Apr 9, 2025
cfe6826
feat(examples): add basic-history-state example dependencies
naoya7076 Apr 9, 2025
6a24a9b
refactor(router): filter internal properties from router state in dev…
naoya7076 Apr 10, 2025
a4783ea
feat(router): add useHistoryState method to LazyRoute class
naoya7076 Apr 14, 2025
d70e920
refactor(router): move locationState declaration outside of select fu…
naoya7076 Apr 14, 2025
6dbe18c
refactor(router): filter out internal properties from locationState i…
naoya7076 Apr 14, 2025
1d1f3ea
feat(router): add FullStateSchema support in RouteMatch and AssetFnCo…
naoya7076 Apr 16, 2025
7600118
feat(router): add stateError handling and strict state validation in …
naoya7076 Apr 21, 2025
f9facc5
feat(router): implement state validation and error handling in solid-…
naoya7076 Apr 21, 2025
c302f73
refactor(router): rename and enhance internal state filtering in Base…
naoya7076 Apr 21, 2025
d34744c
refactor(router): update state filtering logic in useHistoryState to …
naoya7076 Apr 21, 2025
f39a786
refactor(router): enhance state params logic in BaseTanStackRouterDev…
naoya7076 Apr 21, 2025
6e6f66b
test(router): add tests for useHistoryState
naoya7076 Apr 26, 2025
d70b562
docs(router): add documentation for useHistoryState hook with options…
naoya7076 Apr 26, 2025
ee478fb
Merge branch 'main' of https://github.com/TanStack/router into add-us…
naoya7076 Apr 27, 2025
c415ee4
feat(router): add state validation and error handling in RouterCore
naoya7076 Apr 27, 2025
52c3fb3
feat(router): add ValidateHistoryState type to exports
naoya7076 Apr 28, 2025
a563918
feat(router): add fullStateSchema to RouteMatch types in Matches.test…
naoya7076 Apr 28, 2025
ea6cf10
feat(router): implement state examples and enhance useHistoryState de…
naoya7076 Apr 28, 2025
c4ed06e
feat(solid-router): add useHistoryState hook and integrate into routi…
naoya7076 Apr 28, 2025
121ff88
Update docs/router/framework/react/api/router/useHistoryStateHook.md
naoya7076 Apr 28, 2025
4957780
Update docs/router/framework/react/api/router/useHistoryStateHook.md
naoya7076 Apr 28, 2025
3f98f34
Merge branch 'main' of https://github.com/TanStack/router into add-us…
naoya7076 May 17, 2025
10d7797
feat: add TStateValidator to UpdatableRouteOptions interface
naoya7076 May 17, 2025
248b543
Merge branch 'main' of https://github.com/TanStack/router into add-us…
naoya7076 May 18, 2025
7fbae85
fix: update stateSchema references to fullStateSchema in MakeRouteMat…
naoya7076 Jun 1, 2025
e3868ff
Merge branch 'main' of https://github.com/TanStack/router into add-us…
naoya7076 Jun 1, 2025
2ed264b
refactor: adjust generic type syntax in Route and RootRoute classes
naoya7076 Jun 7, 2025
1667135
refactor: update basic-history-state example
naoya7076 Jun 8, 2025
ab6556e
feat: add StateSchemaInput type
naoya7076 Jun 8, 2025
429537d
refactor: update RouteMatch types and align stateSchema location with…
naoya7076 Jun 8, 2025
5dafdd5
fix: correct fullStateSchema position in RouteMatch types
naoya7076 Jun 9, 2025
d020162
Merge branch 'main' into add-usehistorystate
naoya7076 Jun 12, 2025
9022125
feat(router): add TStateValidator to FileRouteOptions and BaseRoute
naoya7076 Jun 12, 2025
c542daa
fix(router): handle undefined rawState in filteredState calculation
naoya7076 Jun 13, 2025
b81f26d
refactor(router): remove unused posts routes and related components
naoya7076 Jun 14, 2025
a7ada2b
update pnpm-lock
naoya7076 Jun 15, 2025
d4d105c
refactor: Consolidate validateState and validateSearch into validateI…
naoya7076 Jun 15, 2025
67f84d1
refactor: consolidate ResolveSearchValidatorInputFn and ResolveStateV…
naoya7076 Jun 15, 2025
88c7126
refactor: Consolidate ResolveStateValidator and ResolveSearchValidator
naoya7076 Jun 15, 2025
7847424
refactor: unify internal state filtering implementation and rename fi…
naoya7076 Jun 16, 2025
782082f
fix: move @tanstack/history from devDependencies to dependencies
naoya7076 Jun 21, 2025
481df10
test: add useHistoryState.text-d.tsx
naoya7076 Jun 22, 2025
9d0d1f7
refactor: change UseHistoryState types and remove unused constraints
naoya7076 Jun 22, 2025
5315139
test: add useHistoryState.text-d.tsx (solid-router)
naoya7076 Jun 22, 2025
f33931d
refactor: fix UseHistoryState types and remove unused definitions
naoya7076 Jun 22, 2025
99fb4d5
Merge branch 'main' into add-usehistorystate
naoya7076 Jun 22, 2025
a7e1cc9
fix: add missing comma in dependencies section of package.json
naoya7076 Jun 22, 2025
799541f
refactor: simplify state filtering by using omitInternalKeys
naoya7076 Jun 22, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 148 additions & 0 deletions docs/router/framework/react/api/router/useHistoryStateHook.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
---
id: useHistoryStateHook
title: useHistoryState hook
---

The `useHistoryState` hook returns the state object that was passed during navigation to the closest match or a specific route match.

## useHistoryState options

The `useHistoryState` hook accepts an optional `options` object.

### `opts.from` option

- Type: `string`
- Optional
- The route ID to get state from. If not provided, the state from the closest match will be used.

### `opts.strict` option

- Type: `boolean`
- Optional - `default: true`
- If `true`, the state object type will be strictly typed based on the route's `validateState`.
- If `false`, the hook returns a loosely typed `Partial<Record<string, unknown>>` object.

### `opts.shouldThrow` option

- Type: `boolean`
- Optional
- `default: true`
- If `false`, `useHistoryState` will not throw an invariant exception in case a match was not found in the currently rendered matches; in this case, it will return `undefined`.

### `opts.select` option

- Optional
- `(state: StateType) => TSelected`
- If supplied, this function will be called with the state object and the return value will be returned from `useHistoryState`. This value will also be used to determine if the hook should re-render its parent component using shallow equality checks.

### `opts.structuralSharing` option

- Type: `boolean`
- Optional
- Configures whether structural sharing is enabled for the value returned by `select`.
- See the [Render Optimizations guide](../../guide/render-optimizations.md) for more information.

## useHistoryState returns

- The state object passed during navigation to the specified route, or `TSelected` if a `select` function is provided.
- Returns `undefined` if no match is found and `shouldThrow` is `false`.

## State Validation

You can validate the state object by defining a `validateState` function on your route:

```tsx
const route = createRoute({
// ...
validateState: (input) =>
z.object({
color: z.enum(['white', 'red', 'green']).catch('white'),
key: z.string().catch(''),
}).parse(input),
})
```

This ensures type safety and validation for your route's state.

## Examples

```tsx
import { useHistoryState } from '@tanstack/react-router'

// Get route API for a specific route
const routeApi = getRouteApi('/posts/$postId')

function Component() {
// Get state from the closest match
const state = useHistoryState()

// OR

// Get state from a specific route
const routeState = useHistoryState({ from: '/posts/$postId' })

// OR

// Use the route API
const apiState = routeApi.useHistoryState()

// OR

// Select a specific property from the state
const color = useHistoryState({
from: '/posts/$postId',
select: (state) => state.color,
})

// OR

// Get state without throwing an error if the match is not found
const optionalState = useHistoryState({ shouldThrow: false })

// ...
}
```

### Complete Example

```tsx
// Define a route with state validation
const postRoute = createRoute({
getParentRoute: () => postsLayoutRoute,
path: 'post',
validateState: (input) =>
z.object({
color: z.enum(['white', 'red', 'green']).catch('white'),
key: z.string().catch(''),
}).parse(input),
component: PostComponent,
})

// Navigate with state
function PostsLayoutComponent() {
return (
<Link
to={postRoute.to}
state={{
color: 'red',
key: 'test-value',
}}
>
View Post
</Link>
)
}

// Use the state in a component
function PostComponent() {
const post = postRoute.useLoaderData()
const { color } = postRoute.useHistoryState()

return (
<div className="space-y-2">
<h4 className="text-xl font-bold">{post.title}</h4>
<h4 style={{ color }}>Colored by state</h4>
</div>
)
}
```
10 changes: 10 additions & 0 deletions examples/react/basic-history-state/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
node_modules
.DS_Store
dist
dist-ssr
*.local

/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
11 changes: 11 additions & 0 deletions examples/react/basic-history-state/.vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"files.watcherExclude": {
"**/routeTree.gen.ts": true
},
"search.exclude": {
"**/routeTree.gen.ts": true
},
"files.readonlyInclude": {
"**/routeTree.gen.ts": true
}
}
6 changes: 6 additions & 0 deletions examples/react/basic-history-state/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Example

To run this example:

- `npm install` or `yarn`
- `npm start` or `yarn start`
12 changes: 12 additions & 0 deletions examples/react/basic-history-state/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
29 changes: 29 additions & 0 deletions examples/react/basic-history-state/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"name": "tanstack-router-react-example-basic-history-state",
"private": true,
"type": "module",
"scripts": {
"dev": "vite --port=3000",
"build": "vite build && tsc --noEmit",
"serve": "vite preview",
"start": "vite"
},
"dependencies": {
"@tanstack/react-router": "^1.114.24",
"@tanstack/react-router-devtools": "^1.114.24",
"autoprefixer": "^10.4.20",
"postcss": "^8.5.1",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"redaxios": "^0.5.1",
"tailwindcss": "^3.4.17",
"zod": "^3.24.2"
},
"devDependencies": {
"@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"
}
}
6 changes: 6 additions & 0 deletions examples/react/basic-history-state/postcss.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
146 changes: 146 additions & 0 deletions examples/react/basic-history-state/src/main.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import {
Link,
Outlet,
RouterProvider,
createRootRoute,
createRoute,
createRouter,
} from '@tanstack/react-router'
import { TanStackRouterDevtools } from '@tanstack/react-router-devtools'
import { z } from 'zod'
import './styles.css'

const rootRoute = createRootRoute({
component: RootComponent,
notFoundComponent: () => {
return <p>This is the notFoundComponent configured on root route</p>
},
})

function RootComponent() {
return (
<div className="bg-gradient-to-r from-green-700 to-lime-600 text-white">
<div className="p-2 flex gap-2 text-lg bg-black/40 shadow-xl">
<Link
to="/"
activeProps={{
className: 'font-bold',
}}
activeOptions={{ exact: true }}
>
Home
</Link>
<Link
to="/state-examples"
activeProps={{
className: 'font-bold',
}}
>
State Examples
</Link>
</div>
<Outlet />
<TanStackRouterDevtools position="bottom-right" />
</div>
)
}
const indexRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/',
component: IndexComponent,
})

function IndexComponent() {
return (
<div className="p-2">
<h3>Welcome Home!</h3>
</div>
)
}

// Route to demonstrate various useHistoryState usages
const stateExamplesRoute = createRoute({
getParentRoute: () => rootRoute,
path: 'state-examples',
component: StateExamplesComponent,
})

const stateDestinationRoute = createRoute({
getParentRoute: () => stateExamplesRoute,
path: 'destination',
validateState: (input: {
example: string
count: number
options: Array<string>
}) =>
z
.object({
example: z.string(),
count: z.number(),
options: z.array(z.string()),
})
.parse(input),
component: StateDestinationComponent,
})

function StateExamplesComponent() {
return (
<div className="p-2">
<h3 className="text-xl font-bold mb-4">useHistoryState Examples</h3>
<div className="flex gap-4">
<Link
to={stateDestinationRoute.to}
state={{
example: 'Test Data',
count: 42,
options: ['Option 1', 'Option 2', 'Option 3'],
}}
className="bg-green-600 px-3 py-2 rounded hover:bg-green-500"
>
Link with State
</Link>
</div>
<Outlet />
</div>
)
}

function StateDestinationComponent() {
const state = stateDestinationRoute.useHistoryState()
return (
<div className="mt-4 p-4 bg-black/20 rounded">
<h4 className="text-lg font-bold mb-2">State Data Display</h4>
<pre className="whitespace-pre-wrap bg-black/30 p-2 rounded text-sm mt-2">
{JSON.stringify(state, null, 2)}
</pre>
</div>
)
}

const routeTree = rootRoute.addChildren([
stateExamplesRoute.addChildren([stateDestinationRoute]),
indexRoute,
])

const router = createRouter({
routeTree,
defaultPreload: 'intent',
defaultStaleTime: 5000,
scrollRestoration: true,
})

declare module '@tanstack/react-router' {
interface Register {
router: typeof router
}
}

const rootElement = document.getElementById('app')!

if (!rootElement.innerHTML) {
const root = ReactDOM.createRoot(rootElement)

root.render(<RouterProvider router={router} />)
}
13 changes: 13 additions & 0 deletions examples/react/basic-history-state/src/styles.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

html {
color-scheme: light dark;
}
* {
@apply border-gray-200 dark:border-gray-800;
}
body {
@apply bg-gray-50 text-gray-950 dark:bg-gray-900 dark:text-gray-200;
}
4 changes: 4 additions & 0 deletions examples/react/basic-history-state/tailwind.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/** @type {import('tailwindcss').Config} */
export default {
content: ['./src/**/*.{js,jsx,ts,tsx}', './index.html'],
}
Loading