Skip to content

feat(router): Add useHistoryState hook for type-safe history 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 33 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 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
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: {},
},
}
Loading