-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
naoya7076
wants to merge
70
commits into
TanStack:main
Choose a base branch
from
naoya7076:add-usehistorystate
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
70 commits
Select commit
Hold shift + click to select a range
c32a680
feat(router): add validateState function for state validation in router
naoya7076 6008e61
feat(router): add TStateValidator type for state validation in routing
naoya7076 5249d78
add TStateValidator to route type definition
naoya7076 9ffaa51
feat(router): add useHistoryState for enhanced state management
naoya7076 1ea838c
feat(router): add state params display in devtools panel
naoya7076 62b2090
feat(router): implement useHistoryState hook for custom state management
naoya7076 79ef275
feat(router): add UseHistoryState types for enhanced state management
naoya7076 cf7ac4d
refactor(router): delete unused type
naoya7076 8ed329b
feat(router): enhance useHistoryState with additional options and imp…
naoya7076 96af77e
refactor(router): update useHistoryState.ts types for improved state …
naoya7076 4f4bc4c
refactor(router): replace useRouterState with useLocation in useHisto…
naoya7076 b3b4d8a
add useHistoryState basic example
naoya7076 cfe6826
feat(examples): add basic-history-state example dependencies
naoya7076 6a24a9b
refactor(router): filter internal properties from router state in dev…
naoya7076 a4783ea
feat(router): add useHistoryState method to LazyRoute class
naoya7076 d70e920
refactor(router): move locationState declaration outside of select fu…
naoya7076 6dbe18c
refactor(router): filter out internal properties from locationState i…
naoya7076 1d1f3ea
feat(router): add FullStateSchema support in RouteMatch and AssetFnCo…
naoya7076 7600118
feat(router): add stateError handling and strict state validation in …
naoya7076 f9facc5
feat(router): implement state validation and error handling in solid-…
naoya7076 c302f73
refactor(router): rename and enhance internal state filtering in Base…
naoya7076 d34744c
refactor(router): update state filtering logic in useHistoryState to …
naoya7076 f39a786
refactor(router): enhance state params logic in BaseTanStackRouterDev…
naoya7076 6e6f66b
test(router): add tests for useHistoryState
naoya7076 d70b562
docs(router): add documentation for useHistoryState hook with options…
naoya7076 ee478fb
Merge branch 'main' of https://github.com/TanStack/router into add-us…
naoya7076 c415ee4
feat(router): add state validation and error handling in RouterCore
naoya7076 52c3fb3
feat(router): add ValidateHistoryState type to exports
naoya7076 a563918
feat(router): add fullStateSchema to RouteMatch types in Matches.test…
naoya7076 ea6cf10
feat(router): implement state examples and enhance useHistoryState de…
naoya7076 c4ed06e
feat(solid-router): add useHistoryState hook and integrate into routi…
naoya7076 121ff88
Update docs/router/framework/react/api/router/useHistoryStateHook.md
naoya7076 4957780
Update docs/router/framework/react/api/router/useHistoryStateHook.md
naoya7076 3f98f34
Merge branch 'main' of https://github.com/TanStack/router into add-us…
naoya7076 10d7797
feat: add TStateValidator to UpdatableRouteOptions interface
naoya7076 248b543
Merge branch 'main' of https://github.com/TanStack/router into add-us…
naoya7076 7fbae85
fix: update stateSchema references to fullStateSchema in MakeRouteMat…
naoya7076 e3868ff
Merge branch 'main' of https://github.com/TanStack/router into add-us…
naoya7076 2ed264b
refactor: adjust generic type syntax in Route and RootRoute classes
naoya7076 1667135
refactor: update basic-history-state example
naoya7076 ab6556e
feat: add StateSchemaInput type
naoya7076 429537d
refactor: update RouteMatch types and align stateSchema location with…
naoya7076 5dafdd5
fix: correct fullStateSchema position in RouteMatch types
naoya7076 d020162
Merge branch 'main' into add-usehistorystate
naoya7076 9022125
feat(router): add TStateValidator to FileRouteOptions and BaseRoute
naoya7076 c542daa
fix(router): handle undefined rawState in filteredState calculation
naoya7076 b81f26d
refactor(router): remove unused posts routes and related components
naoya7076 a7ada2b
update pnpm-lock
naoya7076 d4d105c
refactor: Consolidate validateState and validateSearch into validateI…
naoya7076 67f84d1
refactor: consolidate ResolveSearchValidatorInputFn and ResolveStateV…
naoya7076 88c7126
refactor: Consolidate ResolveStateValidator and ResolveSearchValidator
naoya7076 7847424
refactor: unify internal state filtering implementation and rename fi…
naoya7076 782082f
fix: move @tanstack/history from devDependencies to dependencies
naoya7076 481df10
test: add useHistoryState.text-d.tsx
naoya7076 9d0d1f7
refactor: change UseHistoryState types and remove unused constraints
naoya7076 5315139
test: add useHistoryState.text-d.tsx (solid-router)
naoya7076 f33931d
refactor: fix UseHistoryState types and remove unused definitions
naoya7076 99fb4d5
Merge branch 'main' into add-usehistorystate
naoya7076 a7e1cc9
fix: add missing comma in dependencies section of package.json
naoya7076 799541f
refactor: simplify state filtering by using omitInternalKeys
naoya7076 4aa1f91
Merge branch 'main' into add-usehistorystate
naoya7076 12fd86e
fix: update lockfile to match package.json
naoya7076 01510ce
ci: apply automated fixes
autofix-ci[bot] f602086
Merge branch 'main' of https://github.com/TanStack/router into add-us…
naoya7076 b883558
refactor: extract index route component in useHistoryState tests
naoya7076 13d15ad
merge: resolve conflicts with upstream/main for useHistoryState PR
naoya7076 ae36ee3
fix: add TStateValidator to FileBaseRouteOptions and fix AnyRoute arity
naoya7076 7ce53bb
fix: update basic-history-state example to Tailwind v4 and sync lockfile
naoya7076 48f03e8
fix: add TStateValidator to vue-router and start-client-core packages
naoya7076 1217ca2
fix: add TFullStateSchema type arg to RouteMatch in vue-router tests
naoya7076 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
152 changes: 152 additions & 0 deletions
152
docs/router/framework/react/api/router/useHistoryStateHook.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,152 @@ | ||
| --- | ||
| 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> | ||
| ) | ||
| } | ||
| ``` | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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/ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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` |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| { | ||
| "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": { | ||
| "@tailwindcss/vite": "^4.1.18", | ||
| "@tanstack/react-router": "^1.158.4", | ||
| "@tanstack/react-router-devtools": "^1.158.4", | ||
| "react": "^19.0.0", | ||
| "react-dom": "^19.0.0", | ||
| "redaxios": "^0.5.1", | ||
| "tailwindcss": "^4.1.18", | ||
| "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": "^7.3.1" | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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} />) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| @import 'tailwindcss' source('../'); | ||
|
|
||
| @layer base { | ||
| *, | ||
| ::after, | ||
| ::before, | ||
| ::backdrop, | ||
| ::file-selector-button { | ||
| border-color: var(--color-gray-200, currentcolor); | ||
| } | ||
| } | ||
|
|
||
| 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; | ||
naoya7076 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.