From a96c71a8d3fcb38e7fdb275713763dd0c940d345 Mon Sep 17 00:00:00 2001
From: Nitzan Savion <59604278+nizans@users.noreply.github.com>
Date: Thu, 12 Jun 2025 03:21:11 +0300
Subject: [PATCH] Add global blocking functionality to router
fix: standardize 'DISMISS_BLOCK' action naming in history module
clean empty line
proceedAll functionality and example
global blocking
---
.../basic-file-based/src/routeTree.gen.ts | 78 ++++++++++++
.../basic-file-based/src/routes/__root.tsx | 8 ++
.../global-blocker/_layout.multi-blockers.tsx | 30 +++++
.../src/routes/global-blocker/_layout.tsx | 52 ++++++++
.../basic-file-based/tests/app.spec.ts | 107 ++++++++++++++++
.../react/global-blocking-state/.gitignore | 10 ++
.../.vscode/settings.json | 11 ++
.../react/global-blocking-state/README.md | 6 +
.../react/global-blocking-state/index.html | 12 ++
.../react/global-blocking-state/package.json | 30 +++++
.../global-blocking-state/postcss.config.mjs | 6 +
.../react/global-blocking-state/src/main.tsx | 26 ++++
.../src/routeTree.gen.ts | 95 ++++++++++++++
.../src/routes/__root.tsx | 119 ++++++++++++++++++
.../src/routes/blocking.index.tsx | 17 +++
.../src/routes/index.tsx | 14 +++
.../src/routes/multi-blockers.index.tsx | 30 +++++
.../global-blocking-state/src/styles.css | 13 ++
.../global-blocking-state/tailwind.config.mjs | 4 +
.../global-blocking-state/tsconfig.dev.json | 10 ++
.../react/global-blocking-state/tsconfig.json | 11 ++
.../global-blocking-state/vite.config.js | 11 ++
packages/history/src/index.ts | 89 ++++++++++++-
packages/react-router/src/index.tsx | 1 +
packages/react-router/src/useBlocker.tsx | 7 +-
.../src/useNavigationBlockingState.ts | 51 ++++++++
packages/router-core/src/router.ts | 1 -
packages/solid-router/src/useBlocker.tsx | 7 +-
pnpm-lock.yaml | 49 ++++++++
...tack-router-e2e-react-basic-file-based.txt | 1 +
30 files changed, 894 insertions(+), 12 deletions(-)
create mode 100644 e2e/react-router/basic-file-based/src/routes/global-blocker/_layout.multi-blockers.tsx
create mode 100644 e2e/react-router/basic-file-based/src/routes/global-blocker/_layout.tsx
create mode 100644 examples/react/global-blocking-state/.gitignore
create mode 100644 examples/react/global-blocking-state/.vscode/settings.json
create mode 100644 examples/react/global-blocking-state/README.md
create mode 100644 examples/react/global-blocking-state/index.html
create mode 100644 examples/react/global-blocking-state/package.json
create mode 100644 examples/react/global-blocking-state/postcss.config.mjs
create mode 100644 examples/react/global-blocking-state/src/main.tsx
create mode 100644 examples/react/global-blocking-state/src/routeTree.gen.ts
create mode 100644 examples/react/global-blocking-state/src/routes/__root.tsx
create mode 100644 examples/react/global-blocking-state/src/routes/blocking.index.tsx
create mode 100644 examples/react/global-blocking-state/src/routes/index.tsx
create mode 100644 examples/react/global-blocking-state/src/routes/multi-blockers.index.tsx
create mode 100644 examples/react/global-blocking-state/src/styles.css
create mode 100644 examples/react/global-blocking-state/tailwind.config.mjs
create mode 100644 examples/react/global-blocking-state/tsconfig.dev.json
create mode 100644 examples/react/global-blocking-state/tsconfig.json
create mode 100644 examples/react/global-blocking-state/vite.config.js
create mode 100644 packages/react-router/src/useNavigationBlockingState.ts
create mode 100644 port-tanstack-router-e2e-react-basic-file-based.txt
diff --git a/e2e/react-router/basic-file-based/src/routeTree.gen.ts b/e2e/react-router/basic-file-based/src/routeTree.gen.ts
index 8150c3354d..38a1e78db3 100644
--- a/e2e/react-router/basic-file-based/src/routeTree.gen.ts
+++ b/e2e/react-router/basic-file-based/src/routeTree.gen.ts
@@ -27,6 +27,7 @@ import { Route as StructuralSharingEnabledRouteImport } from './routes/structura
import { Route as SearchParamsDefaultRouteImport } from './routes/search-params/default'
import { Route as RedirectTargetRouteImport } from './routes/redirect/$target'
import { Route as PostsPostIdRouteImport } from './routes/posts.$postId'
+import { Route as GlobalBlockerLayoutRouteImport } from './routes/global-blocker/_layout'
import { Route as LayoutLayout2RouteImport } from './routes/_layout/_layout-2'
import { Route as groupLazyinsideRouteImport } from './routes/(group)/lazyinside'
import { Route as groupInsideRouteImport } from './routes/(group)/inside'
@@ -48,13 +49,20 @@ import { Route as ParamsPsWildcardSplatRouteImport } from './routes/params-ps/wi
import { Route as ParamsPsNamedChar123fooChar125suffixRouteImport } from './routes/params-ps/named/{$foo}suffix'
import { Route as ParamsPsNamedPrefixChar123fooChar125RouteImport } from './routes/params-ps/named/prefix{$foo}'
import { Route as ParamsPsNamedFooRouteImport } from './routes/params-ps/named/$foo'
+import { Route as GlobalBlockerLayoutMultiBlockersRouteImport } from './routes/global-blocker/_layout.multi-blockers'
import { Route as LayoutLayout2LayoutBRouteImport } from './routes/_layout/_layout-2/layout-b'
import { Route as LayoutLayout2LayoutARouteImport } from './routes/_layout/_layout-2/layout-a'
import { Route as groupSubfolderInsideRouteImport } from './routes/(group)/subfolder/inside'
import { Route as groupLayoutInsidelayoutRouteImport } from './routes/(group)/_layout.insidelayout'
+const GlobalBlockerRouteImport = createFileRoute('/global-blocker')()
const groupRouteImport = createFileRoute('/(group)')()
+const GlobalBlockerRoute = GlobalBlockerRouteImport.update({
+ id: '/global-blocker',
+ path: '/global-blocker',
+ getParentRoute: () => rootRouteImport,
+} as any)
const groupRoute = groupRouteImport.update({
id: '/(group)',
getParentRoute: () => rootRouteImport,
@@ -140,6 +148,10 @@ const PostsPostIdRoute = PostsPostIdRouteImport.update({
path: '/$postId',
getParentRoute: () => PostsRoute,
} as any)
+const GlobalBlockerLayoutRoute = GlobalBlockerLayoutRouteImport.update({
+ id: '/_layout',
+ getParentRoute: () => GlobalBlockerRoute,
+} as any)
const LayoutLayout2Route = LayoutLayout2RouteImport.update({
id: '/_layout-2',
getParentRoute: () => LayoutRoute,
@@ -251,6 +263,12 @@ const ParamsPsNamedFooRoute = ParamsPsNamedFooRouteImport.update({
path: '/params-ps/named/$foo',
getParentRoute: () => rootRouteImport,
} as any)
+const GlobalBlockerLayoutMultiBlockersRoute =
+ GlobalBlockerLayoutMultiBlockersRouteImport.update({
+ id: '/multi-blockers',
+ path: '/multi-blockers',
+ getParentRoute: () => GlobalBlockerLayoutRoute,
+ } as any)
const LayoutLayout2LayoutBRoute = LayoutLayout2LayoutBRouteImport.update({
id: '/layout-b',
path: '/layout-b',
@@ -283,6 +301,7 @@ export interface FileRoutesByFullPath {
'/onlyrouteinside': typeof anotherGroupOnlyrouteinsideRoute
'/inside': typeof groupInsideRoute
'/lazyinside': typeof groupLazyinsideRoute
+ '/global-blocker': typeof GlobalBlockerLayoutRouteWithChildren
'/posts/$postId': typeof PostsPostIdRoute
'/redirect/$target': typeof RedirectTargetRouteWithChildren
'/search-params/default': typeof SearchParamsDefaultRoute
@@ -295,6 +314,7 @@ export interface FileRoutesByFullPath {
'/subfolder/inside': typeof groupSubfolderInsideRoute
'/layout-a': typeof LayoutLayout2LayoutARoute
'/layout-b': typeof LayoutLayout2LayoutBRoute
+ '/global-blocker/multi-blockers': typeof GlobalBlockerLayoutMultiBlockersRoute
'/params-ps/named/$foo': typeof ParamsPsNamedFooRoute
'/params-ps/named/prefix{$foo}': typeof ParamsPsNamedPrefixChar123fooChar125Route
'/params-ps/named/{$foo}suffix': typeof ParamsPsNamedChar123fooChar125suffixRoute
@@ -321,6 +341,7 @@ export interface FileRoutesByTo {
'/onlyrouteinside': typeof anotherGroupOnlyrouteinsideRoute
'/inside': typeof groupInsideRoute
'/lazyinside': typeof groupLazyinsideRoute
+ '/global-blocker': typeof GlobalBlockerLayoutRouteWithChildren
'/posts/$postId': typeof PostsPostIdRoute
'/search-params/default': typeof SearchParamsDefaultRoute
'/structural-sharing/$enabled': typeof StructuralSharingEnabledRoute
@@ -332,6 +353,7 @@ export interface FileRoutesByTo {
'/subfolder/inside': typeof groupSubfolderInsideRoute
'/layout-a': typeof LayoutLayout2LayoutARoute
'/layout-b': typeof LayoutLayout2LayoutBRoute
+ '/global-blocker/multi-blockers': typeof GlobalBlockerLayoutMultiBlockersRoute
'/params-ps/named/$foo': typeof ParamsPsNamedFooRoute
'/params-ps/named/prefix{$foo}': typeof ParamsPsNamedPrefixChar123fooChar125Route
'/params-ps/named/{$foo}suffix': typeof ParamsPsNamedChar123fooChar125suffixRoute
@@ -365,6 +387,8 @@ export interface FileRoutesById {
'/(group)/inside': typeof groupInsideRoute
'/(group)/lazyinside': typeof groupLazyinsideRoute
'/_layout/_layout-2': typeof LayoutLayout2RouteWithChildren
+ '/global-blocker': typeof GlobalBlockerRouteWithChildren
+ '/global-blocker/_layout': typeof GlobalBlockerLayoutRouteWithChildren
'/posts/$postId': typeof PostsPostIdRoute
'/redirect/$target': typeof RedirectTargetRouteWithChildren
'/search-params/default': typeof SearchParamsDefaultRoute
@@ -377,6 +401,7 @@ export interface FileRoutesById {
'/(group)/subfolder/inside': typeof groupSubfolderInsideRoute
'/_layout/_layout-2/layout-a': typeof LayoutLayout2LayoutARoute
'/_layout/_layout-2/layout-b': typeof LayoutLayout2LayoutBRoute
+ '/global-blocker/_layout/multi-blockers': typeof GlobalBlockerLayoutMultiBlockersRoute
'/params-ps/named/$foo': typeof ParamsPsNamedFooRoute
'/params-ps/named/prefix{$foo}': typeof ParamsPsNamedPrefixChar123fooChar125Route
'/params-ps/named/{$foo}suffix': typeof ParamsPsNamedChar123fooChar125suffixRoute
@@ -407,6 +432,7 @@ export interface FileRouteTypes {
| '/onlyrouteinside'
| '/inside'
| '/lazyinside'
+ | '/global-blocker'
| '/posts/$postId'
| '/redirect/$target'
| '/search-params/default'
@@ -419,6 +445,7 @@ export interface FileRouteTypes {
| '/subfolder/inside'
| '/layout-a'
| '/layout-b'
+ | '/global-blocker/multi-blockers'
| '/params-ps/named/$foo'
| '/params-ps/named/prefix{$foo}'
| '/params-ps/named/{$foo}suffix'
@@ -445,6 +472,7 @@ export interface FileRouteTypes {
| '/onlyrouteinside'
| '/inside'
| '/lazyinside'
+ | '/global-blocker'
| '/posts/$postId'
| '/search-params/default'
| '/structural-sharing/$enabled'
@@ -456,6 +484,7 @@ export interface FileRouteTypes {
| '/subfolder/inside'
| '/layout-a'
| '/layout-b'
+ | '/global-blocker/multi-blockers'
| '/params-ps/named/$foo'
| '/params-ps/named/prefix{$foo}'
| '/params-ps/named/{$foo}suffix'
@@ -488,6 +517,8 @@ export interface FileRouteTypes {
| '/(group)/inside'
| '/(group)/lazyinside'
| '/_layout/_layout-2'
+ | '/global-blocker'
+ | '/global-blocker/_layout'
| '/posts/$postId'
| '/redirect/$target'
| '/search-params/default'
@@ -500,6 +531,7 @@ export interface FileRouteTypes {
| '/(group)/subfolder/inside'
| '/_layout/_layout-2/layout-a'
| '/_layout/_layout-2/layout-b'
+ | '/global-blocker/_layout/multi-blockers'
| '/params-ps/named/$foo'
| '/params-ps/named/prefix{$foo}'
| '/params-ps/named/{$foo}suffix'
@@ -529,6 +561,7 @@ export interface RootRouteChildren {
Char45824Char54620Char48124Char44397Route: typeof Char45824Char54620Char48124Char44397Route
anotherGroupOnlyrouteinsideRoute: typeof anotherGroupOnlyrouteinsideRoute
groupRoute: typeof groupRouteWithChildren
+ GlobalBlockerRoute: typeof GlobalBlockerRouteWithChildren
RedirectTargetRoute: typeof RedirectTargetRouteWithChildren
StructuralSharingEnabledRoute: typeof StructuralSharingEnabledRoute
ParamsPsIndexRoute: typeof ParamsPsIndexRoute
@@ -550,6 +583,13 @@ export interface RootRouteChildren {
declare module '@tanstack/react-router' {
interface FileRoutesByPath {
+ '/global-blocker': {
+ id: '/global-blocker'
+ path: '/global-blocker'
+ fullPath: '/global-blocker'
+ preLoaderRoute: typeof GlobalBlockerRouteImport
+ parentRoute: typeof rootRouteImport
+ }
'/(group)': {
id: '/(group)'
path: '/'
@@ -669,6 +709,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof PostsPostIdRouteImport
parentRoute: typeof PostsRoute
}
+ '/global-blocker/_layout': {
+ id: '/global-blocker/_layout'
+ path: '/global-blocker'
+ fullPath: '/global-blocker'
+ preLoaderRoute: typeof GlobalBlockerLayoutRouteImport
+ parentRoute: typeof GlobalBlockerRoute
+ }
'/_layout/_layout-2': {
id: '/_layout/_layout-2'
path: ''
@@ -816,6 +863,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof ParamsPsNamedFooRouteImport
parentRoute: typeof rootRouteImport
}
+ '/global-blocker/_layout/multi-blockers': {
+ id: '/global-blocker/_layout/multi-blockers'
+ path: '/multi-blockers'
+ fullPath: '/global-blocker/multi-blockers'
+ preLoaderRoute: typeof GlobalBlockerLayoutMultiBlockersRouteImport
+ parentRoute: typeof GlobalBlockerLayoutRoute
+ }
'/_layout/_layout-2/layout-b': {
id: '/_layout/_layout-2/layout-b'
path: '/layout-b'
@@ -925,6 +979,29 @@ const groupRouteChildren: groupRouteChildren = {
const groupRouteWithChildren = groupRoute._addFileChildren(groupRouteChildren)
+interface GlobalBlockerLayoutRouteChildren {
+ GlobalBlockerLayoutMultiBlockersRoute: typeof GlobalBlockerLayoutMultiBlockersRoute
+}
+
+const GlobalBlockerLayoutRouteChildren: GlobalBlockerLayoutRouteChildren = {
+ GlobalBlockerLayoutMultiBlockersRoute: GlobalBlockerLayoutMultiBlockersRoute,
+}
+
+const GlobalBlockerLayoutRouteWithChildren =
+ GlobalBlockerLayoutRoute._addFileChildren(GlobalBlockerLayoutRouteChildren)
+
+interface GlobalBlockerRouteChildren {
+ GlobalBlockerLayoutRoute: typeof GlobalBlockerLayoutRouteWithChildren
+}
+
+const GlobalBlockerRouteChildren: GlobalBlockerRouteChildren = {
+ GlobalBlockerLayoutRoute: GlobalBlockerLayoutRouteWithChildren,
+}
+
+const GlobalBlockerRouteWithChildren = GlobalBlockerRoute._addFileChildren(
+ GlobalBlockerRouteChildren,
+)
+
interface RedirectTargetRouteChildren {
RedirectTargetViaBeforeLoadRoute: typeof RedirectTargetViaBeforeLoadRoute
RedirectTargetViaLoaderRoute: typeof RedirectTargetViaLoaderRoute
@@ -953,6 +1030,7 @@ const rootRouteChildren: RootRouteChildren = {
Char45824Char54620Char48124Char44397Route,
anotherGroupOnlyrouteinsideRoute: anotherGroupOnlyrouteinsideRoute,
groupRoute: groupRouteWithChildren,
+ GlobalBlockerRoute: GlobalBlockerRouteWithChildren,
RedirectTargetRoute: RedirectTargetRouteWithChildren,
StructuralSharingEnabledRoute: StructuralSharingEnabledRoute,
ParamsPsIndexRoute: ParamsPsIndexRoute,
diff --git a/e2e/react-router/basic-file-based/src/routes/__root.tsx b/e2e/react-router/basic-file-based/src/routes/__root.tsx
index 4062165c86..d97db6fd4b 100644
--- a/e2e/react-router/basic-file-based/src/routes/__root.tsx
+++ b/e2e/react-router/basic-file-based/src/routes/__root.tsx
@@ -131,6 +131,14 @@ function RootComponent() {
>
This Route Does Not Exist
+
+ Multi Blockers
+
diff --git a/e2e/react-router/basic-file-based/src/routes/global-blocker/_layout.multi-blockers.tsx b/e2e/react-router/basic-file-based/src/routes/global-blocker/_layout.multi-blockers.tsx
new file mode 100644
index 0000000000..8512cd8647
--- /dev/null
+++ b/e2e/react-router/basic-file-based/src/routes/global-blocker/_layout.multi-blockers.tsx
@@ -0,0 +1,30 @@
+import * as React from 'react'
+import { createFileRoute, useBlocker } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/global-blocker/_layout/multi-blockers')({
+ component: MultiBlockersPage,
+})
+
+function MultiBlockersPage() {
+ const blocker1 = useBlocker({
+ shouldBlockFn: async () => Promise.resolve(true),
+ enableBeforeUnload: true,
+ disabled: false,
+ withResolver: true,
+ })
+
+ const blocker2 = useBlocker({
+ shouldBlockFn: async () => Promise.resolve(true),
+ enableBeforeUnload: true,
+ disabled: false,
+ withResolver: true,
+ })
+
+ return (
+
+
This page always blocks navigation
+
blocker1 is {blocker1.status}
+
blocker2 is {blocker2.status}
+
+ )
+}
diff --git a/e2e/react-router/basic-file-based/src/routes/global-blocker/_layout.tsx b/e2e/react-router/basic-file-based/src/routes/global-blocker/_layout.tsx
new file mode 100644
index 0000000000..7c2e0094a4
--- /dev/null
+++ b/e2e/react-router/basic-file-based/src/routes/global-blocker/_layout.tsx
@@ -0,0 +1,52 @@
+import {
+ Outlet,
+ createFileRoute,
+ useNavigationBlockingState,
+} from '@tanstack/react-router'
+
+export const Route = createFileRoute('/global-blocker/_layout')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ const { proceed, reset, status, proceedAll } = useNavigationBlockingState()
+
+ return (
+
+ {status === 'blocked' ? (
+
+
Global Blocking Modal
+
Navigation is blocked
+
+
+
+
+
+
+ ) : null}
+
+
+ )
+}
diff --git a/e2e/react-router/basic-file-based/tests/app.spec.ts b/e2e/react-router/basic-file-based/tests/app.spec.ts
index d41ed601c2..4643746a47 100644
--- a/e2e/react-router/basic-file-based/tests/app.spec.ts
+++ b/e2e/react-router/basic-file-based/tests/app.spec.ts
@@ -107,6 +107,113 @@ test('legacy Proceeding through blocked navigation works', async ({ page }) => {
await expect(page.getByRole('heading')).toContainText('Editing A')
})
+test('useNavigationBlockingState exposes state and resets navigation', async ({
+ page,
+}) => {
+ await page.goto('/global-blocker/multi-blockers')
+
+ const blocker1Status = page.getByTestId('blocker-1-status')
+ const blocker2Status = page.getByTestId('blocker-2-status')
+ const globalBlockingModalHeading = page.getByRole('heading', {
+ name: 'Global Blocking Modal',
+ exact: true,
+ })
+ const resetButton = page.getByRole('button', { name: 'Reset' })
+
+ await expect(page.getByRole('heading')).toContainText(
+ 'This page always blocks navigation',
+ )
+ await expect(blocker1Status).toHaveText('blocker1 is idle')
+ await expect(blocker2Status).toHaveText('blocker2 is idle')
+
+ await page.getByRole('link', { name: 'Home', exact: true }).click()
+
+ await expect(blocker1Status).toHaveText('blocker1 is blocked')
+ await expect(blocker2Status).toHaveText('blocker2 is idle')
+ await expect(globalBlockingModalHeading).toBeVisible()
+
+ await resetButton.click()
+
+ await expect(globalBlockingModalHeading).not.toBeVisible()
+ await expect(blocker1Status).toHaveText('blocker1 is idle')
+ await expect(blocker2Status).toHaveText('blocker2 is idle')
+})
+
+test('useNavigationBlockingState exposes state and proceed navigation', async ({
+ page,
+}) => {
+ await page.goto('/global-blocker/multi-blockers')
+
+ const blocker1Status = page.getByTestId('blocker-1-status')
+ const blocker2Status = page.getByTestId('blocker-2-status')
+ const globalBlockingModalHeading = page.getByRole('heading', {
+ name: 'Global Blocking Modal',
+ exact: true,
+ })
+ const proceedButton = page.getByRole('button', {
+ name: 'Proceed',
+ exact: true,
+ })
+
+ await expect(page.getByRole('heading')).toContainText(
+ 'This page always blocks navigation',
+ )
+ await expect(blocker1Status).toHaveText('blocker1 is idle')
+ await expect(blocker2Status).toHaveText('blocker2 is idle')
+
+ await page.getByRole('link', { name: 'Home', exact: true }).click()
+
+ await expect(blocker1Status).toHaveText('blocker1 is blocked')
+ await expect(blocker2Status).toHaveText('blocker2 is idle')
+ await expect(globalBlockingModalHeading).toBeVisible()
+
+ await proceedButton.click()
+
+ await expect(blocker1Status).toHaveText('blocker1 is idle')
+ await expect(blocker2Status).toHaveText('blocker2 is blocked')
+ await expect(globalBlockingModalHeading).toBeVisible()
+
+ await proceedButton.click()
+
+ await expect(globalBlockingModalHeading).not.toBeVisible()
+ await expect(
+ page.getByRole('heading', { name: 'Welcome Home!', exact: true }),
+ ).toBeVisible()
+})
+
+test('useNavigationBlockingState exposes state and proceed navigation using proceedAll', async ({
+ page,
+}) => {
+ await page.goto('/global-blocker/multi-blockers')
+
+ const blocker1Status = page.getByTestId('blocker-1-status')
+ const blocker2Status = page.getByTestId('blocker-2-status')
+ const globalBlockingModalHeading = page.getByRole('heading', {
+ name: 'Global Blocking Modal',
+ exact: true,
+ })
+ const proceedAllButton = page.getByRole('button', { name: 'Proceed All' })
+
+ await expect(page.getByRole('heading')).toContainText(
+ 'This page always blocks navigation',
+ )
+ await expect(blocker1Status).toHaveText('blocker1 is idle')
+ await expect(blocker2Status).toHaveText('blocker2 is idle')
+
+ await page.getByRole('link', { name: 'Home', exact: true }).click()
+
+ await expect(blocker1Status).toHaveText('blocker1 is blocked')
+ await expect(blocker2Status).toHaveText('blocker2 is idle')
+ await expect(globalBlockingModalHeading).toBeVisible()
+
+ await proceedAllButton.click()
+
+ await expect(globalBlockingModalHeading).not.toBeVisible()
+ await expect(
+ page.getByRole('heading', { name: 'Welcome Home!', exact: true }),
+ ).toBeVisible()
+})
+
test('useCanGoBack correctly disables back button', async ({ page }) => {
const getBackButtonDisabled = async () => {
const backButton = page.getByTestId('back-button')
diff --git a/examples/react/global-blocking-state/.gitignore b/examples/react/global-blocking-state/.gitignore
new file mode 100644
index 0000000000..a6ea47e508
--- /dev/null
+++ b/examples/react/global-blocking-state/.gitignore
@@ -0,0 +1,10 @@
+node_modules
+.DS_Store
+dist
+dist-ssr
+*.local
+
+/test-results/
+/playwright-report/
+/blob-report/
+/playwright/.cache/
diff --git a/examples/react/global-blocking-state/.vscode/settings.json b/examples/react/global-blocking-state/.vscode/settings.json
new file mode 100644
index 0000000000..00b5278e58
--- /dev/null
+++ b/examples/react/global-blocking-state/.vscode/settings.json
@@ -0,0 +1,11 @@
+{
+ "files.watcherExclude": {
+ "**/routeTree.gen.ts": true
+ },
+ "search.exclude": {
+ "**/routeTree.gen.ts": true
+ },
+ "files.readonlyInclude": {
+ "**/routeTree.gen.ts": true
+ }
+}
diff --git a/examples/react/global-blocking-state/README.md b/examples/react/global-blocking-state/README.md
new file mode 100644
index 0000000000..115199d292
--- /dev/null
+++ b/examples/react/global-blocking-state/README.md
@@ -0,0 +1,6 @@
+# Example
+
+To run this example:
+
+- `npm install` or `yarn`
+- `npm start` or `yarn start`
diff --git a/examples/react/global-blocking-state/index.html b/examples/react/global-blocking-state/index.html
new file mode 100644
index 0000000000..9b6335c0ac
--- /dev/null
+++ b/examples/react/global-blocking-state/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ Vite App
+
+
+
+
+
+
diff --git a/examples/react/global-blocking-state/package.json b/examples/react/global-blocking-state/package.json
new file mode 100644
index 0000000000..7b27802bc4
--- /dev/null
+++ b/examples/react/global-blocking-state/package.json
@@ -0,0 +1,30 @@
+{
+ "name": "tanstack-router-react-example-global-blocking-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.120.20",
+ "@tanstack/react-router-devtools": "^1.120.20",
+ "@tanstack/router-plugin": "^1.120.20",
+ "react": "^19.0.0",
+ "react-dom": "^19.0.0",
+ "redaxios": "^0.5.1",
+ "postcss": "^8.5.1",
+ "autoprefixer": "^10.4.20",
+ "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"
+ }
+}
\ No newline at end of file
diff --git a/examples/react/global-blocking-state/postcss.config.mjs b/examples/react/global-blocking-state/postcss.config.mjs
new file mode 100644
index 0000000000..2e7af2b7f1
--- /dev/null
+++ b/examples/react/global-blocking-state/postcss.config.mjs
@@ -0,0 +1,6 @@
+export default {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+}
diff --git a/examples/react/global-blocking-state/src/main.tsx b/examples/react/global-blocking-state/src/main.tsx
new file mode 100644
index 0000000000..a6c8b068ac
--- /dev/null
+++ b/examples/react/global-blocking-state/src/main.tsx
@@ -0,0 +1,26 @@
+import React from 'react'
+import ReactDOM from 'react-dom/client'
+import { RouterProvider, createRouter } from '@tanstack/react-router'
+import { routeTree } from './routeTree.gen'
+import './styles.css'
+
+const router = createRouter({
+ routeTree,
+ defaultPreload: 'intent',
+ context: {},
+ defaultPreloadStaleTime: 0,
+ 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()
+}
diff --git a/examples/react/global-blocking-state/src/routeTree.gen.ts b/examples/react/global-blocking-state/src/routeTree.gen.ts
new file mode 100644
index 0000000000..4f8c592672
--- /dev/null
+++ b/examples/react/global-blocking-state/src/routeTree.gen.ts
@@ -0,0 +1,95 @@
+/* eslint-disable */
+
+// @ts-nocheck
+
+// noinspection JSUnusedGlobalSymbols
+
+// This file was automatically generated by TanStack Router.
+// You should NOT make any changes in this file as it will be overwritten.
+// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
+
+import { Route as rootRouteImport } from './routes/__root'
+import { Route as IndexRouteImport } from './routes/index'
+import { Route as MultiBlockersIndexRouteImport } from './routes/multi-blockers.index'
+import { Route as BlockingIndexRouteImport } from './routes/blocking.index'
+
+const IndexRoute = IndexRouteImport.update({
+ id: '/',
+ path: '/',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const MultiBlockersIndexRoute = MultiBlockersIndexRouteImport.update({
+ id: '/multi-blockers/',
+ path: '/multi-blockers/',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const BlockingIndexRoute = BlockingIndexRouteImport.update({
+ id: '/blocking/',
+ path: '/blocking/',
+ getParentRoute: () => rootRouteImport,
+} as any)
+
+export interface FileRoutesByFullPath {
+ '/': typeof IndexRoute
+ '/blocking': typeof BlockingIndexRoute
+ '/multi-blockers': typeof MultiBlockersIndexRoute
+}
+export interface FileRoutesByTo {
+ '/': typeof IndexRoute
+ '/blocking': typeof BlockingIndexRoute
+ '/multi-blockers': typeof MultiBlockersIndexRoute
+}
+export interface FileRoutesById {
+ __root__: typeof rootRouteImport
+ '/': typeof IndexRoute
+ '/blocking/': typeof BlockingIndexRoute
+ '/multi-blockers/': typeof MultiBlockersIndexRoute
+}
+export interface FileRouteTypes {
+ fileRoutesByFullPath: FileRoutesByFullPath
+ fullPaths: '/' | '/blocking' | '/multi-blockers'
+ fileRoutesByTo: FileRoutesByTo
+ to: '/' | '/blocking' | '/multi-blockers'
+ id: '__root__' | '/' | '/blocking/' | '/multi-blockers/'
+ fileRoutesById: FileRoutesById
+}
+export interface RootRouteChildren {
+ IndexRoute: typeof IndexRoute
+ BlockingIndexRoute: typeof BlockingIndexRoute
+ MultiBlockersIndexRoute: typeof MultiBlockersIndexRoute
+}
+
+declare module '@tanstack/react-router' {
+ interface FileRoutesByPath {
+ '/': {
+ id: '/'
+ path: '/'
+ fullPath: '/'
+ preLoaderRoute: typeof IndexRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/multi-blockers/': {
+ id: '/multi-blockers/'
+ path: '/multi-blockers'
+ fullPath: '/multi-blockers'
+ preLoaderRoute: typeof MultiBlockersIndexRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/blocking/': {
+ id: '/blocking/'
+ path: '/blocking'
+ fullPath: '/blocking'
+ preLoaderRoute: typeof BlockingIndexRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ }
+}
+
+const rootRouteChildren: RootRouteChildren = {
+ IndexRoute: IndexRoute,
+ BlockingIndexRoute: BlockingIndexRoute,
+ MultiBlockersIndexRoute: MultiBlockersIndexRoute,
+}
+export const routeTree = rootRouteImport
+ ._addFileChildren(rootRouteChildren)
+ ._addFileTypes()
diff --git a/examples/react/global-blocking-state/src/routes/__root.tsx b/examples/react/global-blocking-state/src/routes/__root.tsx
new file mode 100644
index 0000000000..a1d3ee6a8e
--- /dev/null
+++ b/examples/react/global-blocking-state/src/routes/__root.tsx
@@ -0,0 +1,119 @@
+import * as React from 'react'
+import {
+ Link,
+ Outlet,
+ createRootRoute,
+ useNavigationBlockingState,
+} from '@tanstack/react-router'
+import { TanStackRouterDevtools } from '@tanstack/react-router-devtools'
+
+export const Route = createRootRoute({
+ component: RootComponent,
+})
+
+function RootComponent() {
+ const { proceed, reset, status, proceedAll } = useNavigationBlockingState()
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {status === 'blocked' ? (
+
+ This modal rendered from __root.tsx, but blocking comes from
+ blocking.index.tsx
+
+
+
+
+ ) : (
+
+ Router not blocking
+
+ )}
+
+
+
+
+
+ >
+ )
+}
diff --git a/examples/react/global-blocking-state/src/routes/blocking.index.tsx b/examples/react/global-blocking-state/src/routes/blocking.index.tsx
new file mode 100644
index 0000000000..60a9d2ef29
--- /dev/null
+++ b/examples/react/global-blocking-state/src/routes/blocking.index.tsx
@@ -0,0 +1,17 @@
+import * as React from 'react'
+import { createFileRoute, useBlocker } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/blocking/')({
+ component: PostsIndexComponent,
+})
+
+function PostsIndexComponent() {
+ useBlocker({
+ shouldBlockFn: async () => Promise.resolve(true),
+ enableBeforeUnload: true,
+ disabled: false,
+ withResolver: true,
+ })
+
+ return This page always blocks navigation
+}
diff --git a/examples/react/global-blocking-state/src/routes/index.tsx b/examples/react/global-blocking-state/src/routes/index.tsx
new file mode 100644
index 0000000000..a80ee027c1
--- /dev/null
+++ b/examples/react/global-blocking-state/src/routes/index.tsx
@@ -0,0 +1,14 @@
+import * as React from 'react'
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/')({
+ component: Home,
+})
+
+function Home() {
+ return (
+
+
This page does not block anything
+
+ )
+}
diff --git a/examples/react/global-blocking-state/src/routes/multi-blockers.index.tsx b/examples/react/global-blocking-state/src/routes/multi-blockers.index.tsx
new file mode 100644
index 0000000000..6ff6052899
--- /dev/null
+++ b/examples/react/global-blocking-state/src/routes/multi-blockers.index.tsx
@@ -0,0 +1,30 @@
+import * as React from 'react'
+import { createFileRoute, useBlocker } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/multi-blockers/')({
+ component: MultiBlockers,
+})
+
+function MultiBlockers() {
+ const blocker1 = useBlocker({
+ shouldBlockFn: async () => Promise.resolve(true),
+ enableBeforeUnload: true,
+ disabled: false,
+ withResolver: true,
+ })
+
+ const blocker2 = useBlocker({
+ shouldBlockFn: async () => Promise.resolve(true),
+ enableBeforeUnload: true,
+ disabled: false,
+ withResolver: true,
+ })
+
+ return (
+
+ This page always blocks navigation
+
blocker1 is {blocker1.status}
+
blocker2 is {blocker2.status}
+
+ )
+}
diff --git a/examples/react/global-blocking-state/src/styles.css b/examples/react/global-blocking-state/src/styles.css
new file mode 100644
index 0000000000..0b8e317099
--- /dev/null
+++ b/examples/react/global-blocking-state/src/styles.css
@@ -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;
+}
diff --git a/examples/react/global-blocking-state/tailwind.config.mjs b/examples/react/global-blocking-state/tailwind.config.mjs
new file mode 100644
index 0000000000..4986094b9d
--- /dev/null
+++ b/examples/react/global-blocking-state/tailwind.config.mjs
@@ -0,0 +1,4 @@
+/** @type {import('tailwindcss').Config} */
+export default {
+ content: ['./src/**/*.{js,jsx,ts,tsx}', './index.html'],
+}
diff --git a/examples/react/global-blocking-state/tsconfig.dev.json b/examples/react/global-blocking-state/tsconfig.dev.json
new file mode 100644
index 0000000000..285a09b0dc
--- /dev/null
+++ b/examples/react/global-blocking-state/tsconfig.dev.json
@@ -0,0 +1,10 @@
+{
+ "composite": true,
+ "extends": "../../../tsconfig.base.json",
+
+ "files": ["src/main.tsx"],
+ "include": [
+ "src"
+ // "__tests__/**/*.test.*"
+ ]
+}
diff --git a/examples/react/global-blocking-state/tsconfig.json b/examples/react/global-blocking-state/tsconfig.json
new file mode 100644
index 0000000000..09e87d501d
--- /dev/null
+++ b/examples/react/global-blocking-state/tsconfig.json
@@ -0,0 +1,11 @@
+{
+ "compilerOptions": {
+ "strict": true,
+ "esModuleInterop": true,
+ "jsx": "react-jsx",
+ "target": "ESNext",
+ "moduleResolution": "Bundler",
+ "lib": ["DOM", "DOM.Iterable", "ES2022"],
+ "skipLibCheck": true
+ }
+}
diff --git a/examples/react/global-blocking-state/vite.config.js b/examples/react/global-blocking-state/vite.config.js
new file mode 100644
index 0000000000..b819774577
--- /dev/null
+++ b/examples/react/global-blocking-state/vite.config.js
@@ -0,0 +1,11 @@
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react'
+import { TanStackRouterVite } from '@tanstack/router-plugin/vite'
+
+// https://vitejs.dev/config/
+export default defineConfig({
+ plugins: [
+ TanStackRouterVite({ target: 'react', autoCodeSplitting: true }),
+ react(),
+ ],
+})
diff --git a/packages/history/src/index.ts b/packages/history/src/index.ts
index 3178cb90bb..5ea470acb2 100644
--- a/packages/history/src/index.ts
+++ b/packages/history/src/index.ts
@@ -8,12 +8,18 @@ export interface NavigateOptions {
type SubscriberHistoryAction =
| {
- type: Exclude
+ type: Exclude
}
| {
type: 'GO'
index: number
}
+ | {
+ type: 'BLOCK'
+ proceedAll: () => void
+ proceed: () => void
+ reset: () => void
+ }
type SubscriberArgs = {
location: HistoryLocation
@@ -60,7 +66,14 @@ export type ParsedHistoryState = HistoryState & {
type ShouldAllowNavigation = any
-export type HistoryAction = 'PUSH' | 'REPLACE' | 'FORWARD' | 'BACK' | 'GO'
+export type HistoryAction =
+ | 'PUSH'
+ | 'REPLACE'
+ | 'FORWARD'
+ | 'BACK'
+ | 'GO'
+ | 'BLOCK'
+ | 'DISMISS_BLOCK'
export type BlockerFnArgs = {
currentLocation: HistoryLocation
@@ -70,7 +83,7 @@ export type BlockerFnArgs = {
export type BlockerFn = (
args: BlockerFnArgs,
-) => Promise | ShouldAllowNavigation
+) => AsyncGenerator<(value: boolean) => void, ShouldAllowNavigation, unknown>
export type NavigationBlocker = {
blockerFn: BlockerFn
@@ -143,11 +156,45 @@ export function createHistory(opts: {
if (typeof document !== 'undefined' && blockers.length && isPushOrReplace) {
for (const blocker of blockers) {
const nextLocation = parseHref(actionInfo.path, actionInfo.state)
- const isBlocked = await blocker.blockerFn({
+
+ const generator = blocker.blockerFn({
currentLocation: location,
nextLocation,
action: actionInfo.type,
})
+
+ let isBlocked = false
+ let blockNotified = false
+ let proceedAllCalled = false
+
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+ while (true) {
+ const { value, done } = await generator.next()
+
+ if (!done) {
+ const resolver = value
+ notify({
+ type: 'BLOCK',
+ proceed: () => resolver(false),
+ reset: () => resolver(true),
+ proceedAll: () => {
+ proceedAllCalled = true
+ resolver(false)
+ },
+ })
+ blockNotified = true
+ }
+
+ if (done) {
+ isBlocked = value
+ if (blockNotified) notify({ type: 'DISMISS_BLOCK' })
+ break
+ }
+ }
+
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+ if (proceedAllCalled) break
+
if (isBlocked) {
opts.onBlocked?.()
return
@@ -431,11 +478,43 @@ export function createBrowserHistory(opts?: {
const blockers = _getBlockers()
if (typeof document !== 'undefined' && blockers.length) {
for (const blocker of blockers) {
- const isBlocked = await blocker.blockerFn({
+ const generator = blocker.blockerFn({
currentLocation,
nextLocation,
action,
})
+
+ let isBlocked = false
+ let blockNotified = false
+ let proceedAllCalled = false
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+ while (true) {
+ const { value, done } = await generator.next()
+
+ if (!done) {
+ const resolver = value
+ history.notify({
+ type: 'BLOCK',
+ proceed: () => resolver(false),
+ reset: () => resolver(true),
+ proceedAll: () => {
+ proceedAllCalled = true
+ resolver(false)
+ },
+ })
+ blockNotified = true
+ }
+
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+ if (proceedAllCalled) break
+
+ if (done) {
+ isBlocked = value
+ if (blockNotified) history.notify({ type: 'DISMISS_BLOCK' })
+ break
+ }
+ }
+
if (isBlocked) {
ignoreNextPop = true
win.history.go(1)
diff --git a/packages/react-router/src/index.tsx b/packages/react-router/src/index.tsx
index 2d32671951..f2061a074f 100644
--- a/packages/react-router/src/index.tsx
+++ b/packages/react-router/src/index.tsx
@@ -297,6 +297,7 @@ export {
export type { UseBlockerOpts, ShouldBlockFn } from './useBlocker'
export { useBlocker, Block } from './useBlocker'
+export { useNavigationBlockingState } from './useNavigationBlockingState'
export { useNavigate, Navigate } from './useNavigate'
diff --git a/packages/react-router/src/useBlocker.tsx b/packages/react-router/src/useBlocker.tsx
index 84a8eba9db..7c6bf37d8f 100644
--- a/packages/react-router/src/useBlocker.tsx
+++ b/packages/react-router/src/useBlocker.tsx
@@ -172,7 +172,7 @@ export function useBlocker(
})
React.useEffect(() => {
- const blockerFnComposed = async (blockerFnArgs: BlockerFnArgs) => {
+ async function* blockerFnComposed(blockerFnArgs: BlockerFnArgs) {
function getLocation(
location: HistoryLocation,
): AnyShouldBlockFnLocation {
@@ -208,8 +208,9 @@ export function useBlocker(
if (!shouldBlock) {
return false
}
-
+ let resolvePromise: (value: boolean) => void = () => {}
const promise = new Promise((resolve) => {
+ resolvePromise = resolve
setResolver({
status: 'blocked',
current,
@@ -219,7 +220,7 @@ export function useBlocker(
reset: () => resolve(true),
})
})
-
+ yield resolvePromise
const canNavigateAsync = await promise
setResolver({
status: 'idle',
diff --git a/packages/react-router/src/useNavigationBlockingState.ts b/packages/react-router/src/useNavigationBlockingState.ts
new file mode 100644
index 0000000000..7604f10feb
--- /dev/null
+++ b/packages/react-router/src/useNavigationBlockingState.ts
@@ -0,0 +1,51 @@
+import React from 'react'
+import { useRouter } from './useRouter'
+import type { RouterHistory } from '@tanstack/history'
+
+type BlockerState = {
+ status: 'idle' | 'blocked'
+ reset: () => void
+ proceed: () => void
+ proceedAll: () => void
+}
+
+const initialState = () =>
+ ({
+ status: 'idle',
+ reset: () => {},
+ proceed: () => {},
+ proceedAll: () => {},
+ }) as const
+
+export const useNavigationBlockingState = () => {
+ const { history } = useRouter()
+ const [state, setState] = React.useState(initialState)
+
+ React.useEffect(() => {
+ if (!history) {
+ return
+ }
+ const unsubscribe = (history as RouterHistory).subscribe((event) => {
+ if (event.action.type === 'BLOCK') {
+ const { proceed, proceedAll, reset } = event.action
+ setState({
+ status: 'blocked',
+ proceed,
+ proceedAll,
+ reset,
+ })
+ return
+ }
+
+ if (event.action.type === 'DISMISS_BLOCK') {
+ setState(initialState())
+ }
+ })
+
+ return () => {
+ unsubscribe()
+ }
+ }, [history])
+
+ return state
+}
diff --git a/packages/router-core/src/router.ts b/packages/router-core/src/router.ts
index 81e6abb24e..15bceadafd 100644
--- a/packages/router-core/src/router.ts
+++ b/packages/router-core/src/router.ts
@@ -780,7 +780,6 @@ export class RouterCore<
viewTransitionPromise?: ControlledPromise
isScrollRestoring = false
isScrollRestorationSetup = false
-
// Must build in constructor
__store!: Store>
options!: PickAsRequired<
diff --git a/packages/solid-router/src/useBlocker.tsx b/packages/solid-router/src/useBlocker.tsx
index 7bc23280e5..f2fe9f8665 100644
--- a/packages/solid-router/src/useBlocker.tsx
+++ b/packages/solid-router/src/useBlocker.tsx
@@ -181,7 +181,7 @@ export function useBlocker(
})
Solid.createEffect(() => {
- const blockerFnComposed = async (blockerFnArgs: BlockerFnArgs) => {
+ async function* blockerFnComposed(blockerFnArgs: BlockerFnArgs) {
function getLocation(
location: HistoryLocation,
): AnyShouldBlockFnLocation {
@@ -217,8 +217,9 @@ export function useBlocker(
if (!shouldBlock) {
return false
}
-
+ let resolvePromise: (value: boolean) => void = () => {}
const promise = new Promise((resolve) => {
+ resolvePromise = resolve
setResolver({
status: 'blocked',
current,
@@ -228,7 +229,7 @@ export function useBlocker(
reset: () => resolve(true),
})
})
-
+ yield resolvePromise
const canNavigateAsync = await promise
setResolver({
status: 'idle',
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 18c05e4864..1917cfc794 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -3423,6 +3423,55 @@ importers:
specifier: 6.3.5
version: 6.3.5(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.7.0)
+ examples/react/global-blocking-state:
+ dependencies:
+ '@tanstack/react-router':
+ specifier: workspace:*
+ version: link:../../../packages/react-router
+ '@tanstack/react-router-devtools':
+ specifier: workspace:^
+ version: link:../../../packages/react-router-devtools
+ '@tanstack/router-plugin':
+ specifier: workspace:*
+ version: link:../../../packages/router-plugin
+ autoprefixer:
+ specifier: ^10.4.20
+ version: 10.4.20(postcss@8.5.3)
+ postcss:
+ specifier: ^8.5.1
+ version: 8.5.3
+ react:
+ specifier: ^19.0.0
+ version: 19.0.0
+ react-dom:
+ specifier: ^19.0.0
+ version: 19.0.0(react@19.0.0)
+ redaxios:
+ specifier: ^0.5.1
+ version: 0.5.1
+ tailwindcss:
+ specifier: ^3.4.17
+ version: 3.4.17
+ zod:
+ specifier: ^3.24.2
+ version: 3.25.32
+ devDependencies:
+ '@types/react':
+ specifier: ^19.0.8
+ version: 19.0.8
+ '@types/react-dom':
+ specifier: ^19.0.3
+ version: 19.0.3(@types/react@19.0.8)
+ '@vitejs/plugin-react':
+ specifier: ^4.3.4
+ version: 4.3.4(vite@6.1.4(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0))
+ typescript:
+ specifier: ^5.7.2
+ version: 5.8.2
+ vite:
+ specifier: 6.1.4
+ version: 6.1.4(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)
+
examples/react/kitchen-sink:
dependencies:
'@tanstack/react-router':
diff --git a/port-tanstack-router-e2e-react-basic-file-based.txt b/port-tanstack-router-e2e-react-basic-file-based.txt
new file mode 100644
index 0000000000..44e74ae391
--- /dev/null
+++ b/port-tanstack-router-e2e-react-basic-file-based.txt
@@ -0,0 +1 @@
+53549
\ No newline at end of file