diff --git a/packages/react-router/eslint.config.ts b/packages/react-router/eslint.config.ts
index a2f1715667..5d879181f7 100644
--- a/packages/react-router/eslint.config.ts
+++ b/packages/react-router/eslint.config.ts
@@ -19,6 +19,7 @@ export default [
'@eslint-react/dom/no-missing-button-type': 'off',
'react-hooks/exhaustive-deps': 'error',
'react-hooks/rules-of-hooks': 'error',
+ '@typescript-eslint/no-unnecessary-condition': 'off',
},
},
]
diff --git a/packages/react-router/src/useBlocker.tsx b/packages/react-router/src/useBlocker.tsx
index 735277d100..649700ac40 100644
--- a/packages/react-router/src/useBlocker.tsx
+++ b/packages/react-router/src/useBlocker.tsx
@@ -176,26 +176,39 @@ export function useBlocker(
function getLocation(
location: HistoryLocation,
): AnyShouldBlockFnLocation {
- const parsedLocation = router.parseLocation(location)
- const matchedRoutes = router.getMatchedRoutes(
- parsedLocation.pathname,
- undefined,
- )
+ const pathname = location.pathname
+
+ const matchedRoutes = router.getMatchedRoutes(pathname, undefined)
+
if (matchedRoutes.foundRoute === undefined) {
- throw new Error(`No route found for location ${location.href}`)
+ return {
+ routeId: '__notFound__',
+ fullPath: pathname,
+ pathname: pathname,
+ params: matchedRoutes.routeParams,
+ search: router.options.parseSearch(location.search),
+ }
}
+
return {
routeId: matchedRoutes.foundRoute.id,
fullPath: matchedRoutes.foundRoute.fullPath,
- pathname: parsedLocation.pathname,
+ pathname: pathname,
params: matchedRoutes.routeParams,
- search: parsedLocation.search,
+ search: router.options.parseSearch(location.search),
}
}
const current = getLocation(blockerFnArgs.currentLocation)
const next = getLocation(blockerFnArgs.nextLocation)
+ if (
+ current.routeId === '__notFound__' &&
+ next.routeId !== '__notFound__'
+ ) {
+ return false
+ }
+
const shouldBlock = await shouldBlockFn({
action: blockerFnArgs.action,
current,
diff --git a/packages/react-router/tests/useBlocker.test.tsx b/packages/react-router/tests/useBlocker.test.tsx
index 3b780775bd..c11cd054f1 100644
--- a/packages/react-router/tests/useBlocker.test.tsx
+++ b/packages/react-router/tests/useBlocker.test.tsx
@@ -7,6 +7,7 @@ import { z } from 'zod'
import {
RouterProvider,
createBrowserHistory,
+ createMemoryHistory,
createRootRoute,
createRoute,
createRouter,
@@ -440,4 +441,156 @@ describe('useBlocker', () => {
expect(window.location.pathname).toBe('/invoices')
})
+
+ test('should allow navigation from 404 page when blocker is active', async () => {
+ const rootRoute = createRootRoute({
+ notFoundComponent: function NotFoundComponent() {
+ const navigate = useNavigate()
+
+ useBlocker({ shouldBlockFn: () => true })
+
+ return (
+ <>
+
Not Found
+
+
+ >
+ )
+ },
+ })
+
+ const indexRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/',
+ component: () => {
+ return (
+ <>
+ Index
+ >
+ )
+ },
+ })
+
+ const postsRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/posts',
+ component: () => {
+ return (
+ <>
+ Posts
+ >
+ )
+ },
+ })
+
+ const router = createRouter({
+ routeTree: rootRoute.addChildren([indexRoute, postsRoute]),
+ history,
+ })
+
+ render()
+
+ await router.navigate({ to: '/non-existent' as any })
+
+ expect(
+ await screen.findByRole('heading', { name: 'Not Found' }),
+ ).toBeInTheDocument()
+
+ expect(window.location.pathname).toBe('/non-existent')
+
+ const homeButton = await screen.findByRole('button', { name: 'Go Home' })
+ fireEvent.click(homeButton)
+
+ expect(
+ await screen.findByRole('heading', { name: 'Index' }),
+ ).toBeInTheDocument()
+
+ expect(window.location.pathname).toBe('/')
+ })
+
+ test('should handle blocker navigation from 404 to another 404', async () => {
+ const rootRoute = createRootRoute({
+ notFoundComponent: function NotFoundComponent() {
+ const navigate = useNavigate()
+
+ useBlocker({ shouldBlockFn: () => true })
+
+ return (
+ <>
+ Not Found
+
+ >
+ )
+ },
+ })
+
+ const indexRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/',
+ component: () => {
+ return (
+ <>
+ Index
+ >
+ )
+ },
+ })
+
+ const router = createRouter({
+ routeTree: rootRoute.addChildren([indexRoute]),
+ history,
+ })
+
+ render()
+
+ await router.navigate({ to: '/non-existent' })
+
+ expect(
+ await screen.findByRole('heading', { name: 'Not Found' }),
+ ).toBeInTheDocument()
+
+ const anotherButton = await screen.findByRole('button', {
+ name: 'Go to Another 404',
+ })
+ fireEvent.click(anotherButton)
+
+ expect(
+ await screen.findByRole('heading', { name: 'Not Found' }),
+ ).toBeInTheDocument()
+
+ expect(window.location.pathname).toBe('/non-existent')
+ })
+
+ test('navigate function should handle external URLs with ignoreBlocker', async () => {
+ const rootRoute = createRootRoute()
+ const indexRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/',
+ component: () => Home
,
+ })
+
+ const router = createRouter({
+ routeTree: rootRoute.addChildren([indexRoute]),
+ history: createMemoryHistory({
+ initialEntries: ['/'],
+ }),
+ })
+
+ await expect(
+ router.navigate({
+ to: 'https://example.com',
+ ignoreBlocker: true,
+ }),
+ ).resolves.toBeUndefined()
+
+ await expect(
+ router.navigate({
+ to: 'https://example.com',
+ }),
+ ).resolves.toBeUndefined()
+ })
})
diff --git a/packages/router-core/src/router.ts b/packages/router-core/src/router.ts
index f6bf304292..e8c1c5951c 100644
--- a/packages/router-core/src/router.ts
+++ b/packages/router-core/src/router.ts
@@ -1778,7 +1778,7 @@ export class RouterCore<
})
}
- navigate: NavigateFn = ({ to, reloadDocument, href, ...rest }) => {
+ navigate: NavigateFn = async ({ to, reloadDocument, href, ...rest }) => {
if (!reloadDocument && href) {
try {
new URL(`${href}`)
@@ -1791,6 +1791,26 @@ export class RouterCore<
const location = this.buildLocation({ to, ...rest } as any)
href = this.history.createHref(location.href)
}
+
+ // Check blockers for external URLs unless ignoreBlocker is true
+ if (!rest.ignoreBlocker) {
+ // Cast to access internal getBlockers method
+ const historyWithBlockers = this.history as any
+ const blockers = historyWithBlockers.getBlockers?.() ?? []
+ for (const blocker of blockers) {
+ if (blocker?.blockerFn) {
+ const shouldBlock = await blocker.blockerFn({
+ currentLocation: this.latestLocation,
+ nextLocation: this.latestLocation, // External URLs don't have a next location in our router
+ action: 'PUSH',
+ })
+ if (shouldBlock) {
+ return Promise.resolve()
+ }
+ }
+ }
+ }
+
if (rest.replace) {
window.location.replace(href)
} else {