Skip to content

Commit 692d685

Browse files
committed
feat: allow navigation from 404 pages when blocker is active
- #4881
1 parent fe43f1b commit 692d685

File tree

2 files changed

+144
-8
lines changed

2 files changed

+144
-8
lines changed

packages/react-router/src/useBlocker.tsx

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -176,26 +176,39 @@ export function useBlocker(
176176
function getLocation(
177177
location: HistoryLocation,
178178
): AnyShouldBlockFnLocation {
179-
const parsedLocation = router.parseLocation(location)
180-
const matchedRoutes = router.getMatchedRoutes(
181-
parsedLocation.pathname,
182-
undefined,
183-
)
179+
const pathname = location.pathname
180+
181+
const matchedRoutes = router.getMatchedRoutes(pathname, undefined)
182+
184183
if (matchedRoutes.foundRoute === undefined) {
185-
throw new Error(`No route found for location ${location.href}`)
184+
return {
185+
routeId: '__notFound__',
186+
fullPath: pathname,
187+
pathname: pathname,
188+
params: matchedRoutes.routeParams || {},
189+
search: router.options.parseSearch(location.search),
190+
}
186191
}
192+
187193
return {
188194
routeId: matchedRoutes.foundRoute.id,
189195
fullPath: matchedRoutes.foundRoute.fullPath,
190-
pathname: parsedLocation.pathname,
196+
pathname: pathname,
191197
params: matchedRoutes.routeParams,
192-
search: parsedLocation.search,
198+
search: router.options.parseSearch(location.search),
193199
}
194200
}
195201

196202
const current = getLocation(blockerFnArgs.currentLocation)
197203
const next = getLocation(blockerFnArgs.nextLocation)
198204

205+
if (
206+
current.routeId === '__notFound__' &&
207+
next.routeId !== '__notFound__'
208+
) {
209+
return false
210+
}
211+
199212
const shouldBlock = await shouldBlockFn({
200213
action: blockerFnArgs.action,
201214
current,

packages/react-router/tests/useBlocker.test.tsx

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -440,4 +440,127 @@ describe('useBlocker', () => {
440440

441441
expect(window.location.pathname).toBe('/invoices')
442442
})
443+
444+
test('should allow navigation from 404 page when blocker is active', async () => {
445+
const rootRoute = createRootRoute({
446+
notFoundComponent: () => {
447+
const navigate = useNavigate()
448+
449+
useBlocker({ shouldBlockFn: () => true })
450+
451+
return (
452+
<>
453+
<h1>Not Found</h1>
454+
<button onClick={() => navigate({ to: '/' })}>Go Home</button>
455+
<button onClick={() => navigate({ to: '/posts' })}>
456+
Go to Posts
457+
</button>
458+
</>
459+
)
460+
},
461+
})
462+
463+
const indexRoute = createRoute({
464+
getParentRoute: () => rootRoute,
465+
path: '/',
466+
component: () => {
467+
return (
468+
<>
469+
<h1>Index</h1>
470+
</>
471+
)
472+
},
473+
})
474+
475+
const postsRoute = createRoute({
476+
getParentRoute: () => rootRoute,
477+
path: '/posts',
478+
component: () => {
479+
return (
480+
<>
481+
<h1>Posts</h1>
482+
</>
483+
)
484+
},
485+
})
486+
487+
const router = createRouter({
488+
routeTree: rootRoute.addChildren([indexRoute, postsRoute]),
489+
history,
490+
})
491+
492+
render(<RouterProvider router={router} />)
493+
494+
await router.navigate({ to: '/non-existent' as any })
495+
496+
expect(
497+
await screen.findByRole('heading', { name: 'Not Found' }),
498+
).toBeInTheDocument()
499+
500+
expect(window.location.pathname).toBe('/non-existent')
501+
502+
const homeButton = await screen.findByRole('button', { name: 'Go Home' })
503+
fireEvent.click(homeButton)
504+
505+
expect(
506+
await screen.findByRole('heading', { name: 'Index' }),
507+
).toBeInTheDocument()
508+
509+
expect(window.location.pathname).toBe('/')
510+
})
511+
512+
test('should handle blocker navigation from 404 to another 404', async () => {
513+
const rootRoute = createRootRoute({
514+
notFoundComponent: () => {
515+
const navigate = useNavigate()
516+
517+
useBlocker({ shouldBlockFn: () => true })
518+
519+
return (
520+
<>
521+
<h1>Not Found</h1>
522+
<button onClick={() => navigate({ to: '/another-404' as any })}>
523+
Go to Another 404
524+
</button>
525+
</>
526+
)
527+
},
528+
})
529+
530+
const indexRoute = createRoute({
531+
getParentRoute: () => rootRoute,
532+
path: '/',
533+
component: () => {
534+
return (
535+
<>
536+
<h1>Index</h1>
537+
</>
538+
)
539+
},
540+
})
541+
542+
const router = createRouter({
543+
routeTree: rootRoute.addChildren([indexRoute]),
544+
history,
545+
})
546+
547+
render(<RouterProvider router={router} />)
548+
549+
await router.navigate({ to: '/non-existent' })
550+
551+
expect(
552+
await screen.findByRole('heading', { name: 'Not Found' }),
553+
).toBeInTheDocument()
554+
555+
const anotherButton = await screen.findByRole('button', {
556+
name: 'Go to Another 404',
557+
})
558+
fireEvent.click(anotherButton)
559+
560+
expect(
561+
await screen.findByRole('heading', { name: 'Not Found' }),
562+
).toBeInTheDocument()
563+
564+
expect(window.location.pathname).toBe('/non-existent')
565+
})
443566
})

0 commit comments

Comments
 (0)