Skip to content

Commit 9c81d5a

Browse files
fix: handle beforeLoad throwing notFound correctly (#6811)
1 parent 9f35318 commit 9c81d5a

40 files changed

+2532
-192
lines changed

docs/router/guide/not-found-errors.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,13 @@ The not-found error above will be handled by the same route or nearest parent ro
166166

167167
If neither the route nor any suitable parent route is found to handle the error, the root route will handle it using TanStack Router's **extremely basic (and purposefully undesirable)** default not-found component that simply renders `<p>Not Found</p>`. It's highly recommended to either attach at least one `notFoundComponent` to the root route or configure a router-wide `defaultNotFoundComponent` to handle not-found errors.
168168

169-
> ⚠️ Throwing a notFound error in a beforeLoad method will always trigger the \_\_root notFoundComponent. Since beforeLoad methods are run prior to the route loader methods, there is no guarantee that any required data for layouts have successfully loaded before the error is thrown.
169+
> ⚠️ When you throw `notFound()` in `beforeLoad`, TanStack Router resolves it the same way as other not-found errors:
170+
>
171+
> - If you pass `routeId`, that route (or the nearest valid ancestor boundary) handles it.
172+
> - If you don't pass `routeId`, the nearest route/ancestor with a `notFoundComponent` handles it (based on the router's mode and matching rules).
173+
> - If no suitable boundary is found, handling falls back to the root/default not-found behavior.
174+
>
175+
> For `beforeLoad`-thrown not-found errors, TanStack Router still runs required parent loaders so the selected not-found boundary can render with the loader data it depends on.
170176
171177
## Specifying Which Routes Handle Not Found Errors
172178

e2e/react-start/basic/src/routeTree.gen.ts

Lines changed: 230 additions & 0 deletions
Large diffs are not rendered by default.

e2e/react-start/basic/src/routes/__root.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,14 @@ function RootDocument({ children }: { children: React.ReactNode }) {
175175
>
176176
redirect
177177
</Link>{' '}
178+
<Link
179+
to="/not-found"
180+
activeProps={{
181+
className: 'font-bold',
182+
}}
183+
>
184+
not-found
185+
</Link>{' '}
178186
<Link
179187
to="/client-only"
180188
activeProps={{
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { createFileRoute, notFound } from '@tanstack/react-router'
2+
import type { NotFoundRouteProps } from '@tanstack/react-router'
3+
4+
export const Route = createFileRoute('/not-found/deep/b/c/d')({
5+
loaderDeps: ({ search }) => ({
6+
throwAt: search.throwAt,
7+
}),
8+
beforeLoad: ({ search }) => {
9+
if (search.throwAt === 'd') {
10+
throw notFound({
11+
data: { source: 'd-beforeLoad' },
12+
})
13+
}
14+
},
15+
loader: ({ deps }) => {
16+
if (deps.throwAt === 'c') {
17+
throw new Error('d-loader-should-not-run-when-c-beforeLoad-throws')
18+
}
19+
return { ready: true }
20+
},
21+
component: RouteComponent,
22+
notFoundComponent: (props: NotFoundRouteProps) => (
23+
<div data-testid="deep-d-notFound-component">
24+
Not Found at /not-found/deep/b/c/d (
25+
{(props.data as { source?: string })?.source ?? 'unknown'})
26+
</div>
27+
),
28+
})
29+
30+
function RouteComponent() {
31+
return (
32+
<div data-testid="deep-d-route-component">
33+
Hello "/not-found/deep/b/c/d"!
34+
</div>
35+
)
36+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { Outlet, createFileRoute, notFound } from '@tanstack/react-router'
2+
import type { NotFoundRouteProps } from '@tanstack/react-router'
3+
4+
export const Route = createFileRoute('/not-found/deep/b/c')({
5+
beforeLoad: ({ search }) => {
6+
if (search.throwAt === 'c') {
7+
throw notFound({
8+
data: { source: 'c-beforeLoad' },
9+
})
10+
}
11+
},
12+
component: () => <Outlet />,
13+
notFoundComponent: (props: NotFoundRouteProps) => (
14+
<div data-testid="deep-c-notFound-component">
15+
Not Found at /not-found/deep/b/c (
16+
{(props.data as { source?: string })?.source ?? 'unknown'})
17+
</div>
18+
),
19+
})
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { Outlet, createFileRoute, notFound } from '@tanstack/react-router'
2+
import type { NotFoundRouteProps } from '@tanstack/react-router'
3+
4+
export const Route = createFileRoute('/not-found/deep/b')({
5+
beforeLoad: ({ search }) => {
6+
if (search.throwAt === 'b') {
7+
throw notFound({
8+
data: { source: 'b-beforeLoad' },
9+
})
10+
}
11+
},
12+
component: () => <Outlet />,
13+
notFoundComponent: (props: NotFoundRouteProps) => (
14+
<div data-testid="deep-b-notFound-component">
15+
Not Found at /not-found/deep/b (
16+
{(props.data as { source?: string })?.source ?? 'unknown'})
17+
</div>
18+
),
19+
})
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { Link, createFileRoute } from '@tanstack/react-router'
2+
3+
export const Route = createFileRoute('/not-found/deep/')({
4+
component: RouteComponent,
5+
})
6+
7+
function RouteComponent() {
8+
return (
9+
<div>
10+
<div className="mb-2">
11+
<Link
12+
from={Route.fullPath}
13+
to="./b/c/d"
14+
search={{ throwAt: 'd' }}
15+
data-testid="deep-throwAt-d"
16+
>
17+
/not-found/deep/b/c/d?throwAt=d
18+
</Link>
19+
</div>
20+
<div className="mb-2">
21+
<Link
22+
from={Route.fullPath}
23+
to="./b/c/d"
24+
search={{ throwAt: 'c' }}
25+
data-testid="deep-throwAt-c"
26+
>
27+
/not-found/deep/b/c/d?throwAt=c
28+
</Link>
29+
</div>
30+
<div className="mb-2">
31+
<Link
32+
from={Route.fullPath}
33+
to="./b/c/d"
34+
search={{ throwAt: 'b' }}
35+
data-testid="deep-throwAt-b"
36+
>
37+
/not-found/deep/b/c/d?throwAt=b
38+
</Link>
39+
</div>
40+
</div>
41+
)
42+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { Outlet, createFileRoute } from '@tanstack/react-router'
2+
import z from 'zod'
3+
4+
export const Route = createFileRoute('/not-found/deep')({
5+
validateSearch: z.object({
6+
throwAt: z.enum(['b', 'c', 'd']).optional(),
7+
}),
8+
component: () => <Outlet />,
9+
})

e2e/react-start/basic/src/routes/not-found/index.tsx

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,36 @@ export const Route = createFileRoute('/not-found/')({
2525
via-loader
2626
</Link>
2727
</div>
28+
<div className="mb-2">
29+
<Link
30+
from={Route.fullPath}
31+
to="./via-beforeLoad-target-root"
32+
preload={preload}
33+
data-testid="via-beforeLoad-target-root"
34+
>
35+
via-beforeLoad-target-root (shows global not-found)
36+
</Link>
37+
</div>
38+
<div className="mb-2">
39+
<Link
40+
from={Route.fullPath}
41+
to="./parent-boundary"
42+
preload={preload}
43+
data-testid="parent-boundary-index"
44+
>
45+
parent-boundary test cases
46+
</Link>
47+
</div>
48+
<div className="mb-2">
49+
<Link
50+
from={Route.fullPath}
51+
to="./deep"
52+
preload={preload}
53+
data-testid="deep-index"
54+
>
55+
deep test cases
56+
</Link>
57+
</div>
2858
</div>
2959
)
3060
},
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { Link, createFileRoute } from '@tanstack/react-router'
2+
3+
export const Route = createFileRoute('/not-found/parent-boundary/')({
4+
component: RouteComponent,
5+
})
6+
7+
function RouteComponent() {
8+
return (
9+
<div>
10+
<div className="mb-2">
11+
<Link
12+
from={Route.fullPath}
13+
to="./via-beforeLoad"
14+
data-testid="parent-boundary-via-beforeLoad-with-routeId"
15+
>
16+
via-beforeLoad (with routeId)
17+
</Link>
18+
</div>
19+
<div className="mb-2">
20+
<Link
21+
from={Route.fullPath}
22+
to="./via-beforeLoad"
23+
search={{ target: 'none' }}
24+
data-testid="parent-boundary-via-beforeLoad-without-routeId"
25+
>
26+
via-beforeLoad (without routeId)
27+
</Link>
28+
</div>
29+
</div>
30+
)
31+
}

0 commit comments

Comments
 (0)