Skip to content

Commit ca1f3bd

Browse files
fix
1 parent 48ba93a commit ca1f3bd

File tree

5 files changed

+107
-11
lines changed

5 files changed

+107
-11
lines changed

packages/router-core/src/load-matches.ts

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ const getNotFoundBoundaryIndex = (
8181
return undefined
8282
}
8383

84-
const requestedRouteId = err.routeId ? String(err.routeId) : undefined
84+
const requestedRouteId = err.routeId
8585
const matchedRootIndex = inner.matches.findIndex(
8686
(m) => m.routeId === inner.router.routeTree.id,
8787
)
@@ -890,7 +890,7 @@ export async function loadMatches(arg: {
890890
updateMatch: UpdateMatchFn
891891
sync?: boolean
892892
}): Promise<Array<MakeRouteMatch>> {
893-
const inner: InnerLoadContext = Object.assign(arg)
893+
const inner: InnerLoadContext = arg
894894
const matchPromises: Array<Promise<AnyRouteMatch>> = []
895895

896896
// make sure the pending component is immediately rendered when hydrating a match that is not SSRed
@@ -941,7 +941,6 @@ export async function loadMatches(arg: {
941941
? Math.min(boundaryIndex + 1, baseMaxIndexExclusive)
942942
: baseMaxIndexExclusive
943943

944-
let firstRedirect: unknown
945944
let firstNotFound: NotFoundError | undefined
946945

947946
for (let i = 0; i < maxIndexExclusive; i++) {
@@ -957,19 +956,15 @@ export async function loadMatches(arg: {
957956
if (result.status !== 'rejected') continue
958957

959958
const reason = result.reason
960-
if (!firstRedirect && isRedirect(reason)) {
961-
firstRedirect = reason
959+
if (isRedirect(reason)) {
960+
throw reason
962961
}
963962
if (!firstNotFound && isNotFound(reason)) {
964963
firstNotFound = reason
965964
}
966965
}
967966
}
968967

969-
if (firstRedirect) {
970-
throw firstRedirect
971-
}
972-
973968
const notFoundToThrow =
974969
firstNotFound ??
975970
(beforeLoadNotFound && !inner.preload ? beforeLoadNotFound : undefined)
@@ -1008,10 +1003,19 @@ export async function loadMatches(arg: {
10081003

10091004
notFoundToThrow.routeId = boundaryMatch.routeId
10101005

1006+
const boundaryIsRoot = boundaryMatch.routeId === inner.router.routeTree.id
1007+
10111008
inner.updateMatch(boundaryMatch.id, (prev) => ({
10121009
...prev,
1013-
status: 'notFound',
1014-
error: notFoundToThrow,
1010+
...(boundaryIsRoot
1011+
? // For root boundary, use globalNotFound so the root component's
1012+
// shell still renders and <Outlet> handles the not-found display.
1013+
// Setting status:'notFound' on root would replace the entire shell,
1014+
// and in Solid/Vue the status update can be lost inside startTransition.
1015+
{ globalNotFound: true }
1016+
: // For non-root boundaries, set status:'notFound' so MatchInner
1017+
// renders the notFoundComponent directly.
1018+
{ status: 'notFound' as const, error: notFoundToThrow }),
10151019
isFetching: false,
10161020
}))
10171021

packages/router-core/src/ssr/ssr-client.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ function hydrateMatch(
2828
match.ssr = deyhydratedMatch.ssr
2929
match.updatedAt = deyhydratedMatch.u
3030
match.error = deyhydratedMatch.e
31+
if (deyhydratedMatch.g !== undefined) {
32+
match.globalNotFound = true
33+
}
3134
}
3235

3336
export async function hydrate(router: AnyRouter): Promise<any> {

packages/router-core/src/ssr/ssr-server.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ export function dehydrateMatch(match: AnyRouteMatch): DehydratedMatch {
4747
['loaderData', 'l'],
4848
['error', 'e'],
4949
['ssr', 'ssr'],
50+
['globalNotFound', 'g'],
5051
] as const
5152

5253
for (const [key, shorthand] of properties) {

packages/router-core/src/ssr/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export interface DehydratedMatch {
99
u: MakeRouteMatch['updatedAt']
1010
s: MakeRouteMatch['status']
1111
ssr?: MakeRouteMatch['ssr']
12+
g?: true
1213
}
1314

1415
export interface DehydratedRouter {

packages/router-core/tests/hydrate.test.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,93 @@ describe('hydrate', () => {
218218
expect(ssr).toBe(true)
219219
})
220220

221+
it('should hydrate globalNotFound when dehydrated flag is present', async () => {
222+
const mockMatches = [
223+
{
224+
id: '/',
225+
routeId: '/',
226+
index: 0,
227+
ssr: undefined,
228+
_nonReactive: {},
229+
},
230+
]
231+
232+
const dehydratedMatches = [
233+
{
234+
i: '/',
235+
s: 'success' as const,
236+
ssr: true,
237+
u: Date.now(),
238+
g: true as const,
239+
},
240+
]
241+
242+
mockRouter.matchRoutes = vi.fn().mockReturnValue(mockMatches)
243+
mockRouter.state.matches = mockMatches
244+
245+
mockWindow.$_TSR = {
246+
router: {
247+
manifest: { routes: {} },
248+
dehydratedData: {},
249+
lastMatchId: '/',
250+
matches: dehydratedMatches,
251+
},
252+
h: vi.fn(),
253+
e: vi.fn(),
254+
c: vi.fn(),
255+
p: vi.fn(),
256+
buffer: [],
257+
initialized: false,
258+
}
259+
260+
await hydrate(mockRouter)
261+
262+
expect((mockMatches[0] as AnyRouteMatch).globalNotFound).toBe(true)
263+
})
264+
265+
it('should leave globalNotFound undefined when dehydrated flag is omitted', async () => {
266+
const mockMatches = [
267+
{
268+
id: '/',
269+
routeId: '/',
270+
index: 0,
271+
ssr: undefined,
272+
_nonReactive: {},
273+
},
274+
]
275+
276+
const dehydratedMatches = [
277+
{
278+
i: '/',
279+
s: 'success' as const,
280+
ssr: true,
281+
u: Date.now(),
282+
},
283+
]
284+
285+
mockRouter.matchRoutes = vi.fn().mockReturnValue(mockMatches)
286+
mockRouter.state.matches = mockMatches
287+
288+
mockWindow.$_TSR = {
289+
router: {
290+
manifest: { routes: {} },
291+
dehydratedData: {},
292+
lastMatchId: '/',
293+
matches: dehydratedMatches,
294+
},
295+
h: vi.fn(),
296+
e: vi.fn(),
297+
c: vi.fn(),
298+
p: vi.fn(),
299+
buffer: [],
300+
initialized: false,
301+
}
302+
303+
await hydrate(mockRouter)
304+
305+
expect((mockMatches[0] as AnyRouteMatch).globalNotFound).toBeUndefined()
306+
})
307+
221308
it('should decode dehydrated match ids before hydration lookup and SPA-mode checks', async () => {
222309
const loadSpy = vi.spyOn(mockRouter, 'load')
223310

0 commit comments

Comments
 (0)