From 8731f23cc7de0d5183bb6bde70a854af6580f151 Mon Sep 17 00:00:00 2001 From: sleitor Date: Tue, 3 Mar 2026 18:31:25 +0000 Subject: [PATCH 1/3] fix(router-core): replace fatal invariant with null-check in SPA mode hydration When hydrating a 404/error page in SPA mode, matches[1] can be undefined (only the root match exists). The previous code used a fatal invariant that threw an uncaught error, causing a white screen of death. Replace with a graceful null-check and console.warn, matching the intent of the ESM build behaviour described in #6806. Fixes #6806 --- packages/router-core/src/ssr/ssr-client.ts | 57 ++++++++++++---------- 1 file changed, 31 insertions(+), 26 deletions(-) diff --git a/packages/router-core/src/ssr/ssr-client.ts b/packages/router-core/src/ssr/ssr-client.ts index 6f7ddb9798c..203537d061c 100644 --- a/packages/router-core/src/ssr/ssr-client.ts +++ b/packages/router-core/src/ssr/ssr-client.ts @@ -255,37 +255,42 @@ export async function hydrate(router: AnyRouter): Promise { // this will prevent that other pending components are rendered but hydration is not blocked if (isSpaMode) { const match = matches[1] - invariant( - match, - 'Expected to find a match below the root match in SPA mode.', - ) - setMatchForcePending(match) + // match can be undefined when hydrating a 404/error page in SPA mode (only root match exists) + // in that case, skip setting up pending state and let router.load() handle it + if (!match) { + console.warn( + 'SPA hydration: no child match found below root match. This can happen during 404/error page hydration.', + ) + } else { + setMatchForcePending(match) - match._displayPending = true - match._nonReactive.displayPendingPromise = loadPromise + match._displayPending = true + match._nonReactive.displayPendingPromise = loadPromise - loadPromise.then(() => { - batch(() => { - // ensure router is not in status 'pending' anymore - // this usually happens in Transitioner but if loading synchronously resolves, - // Transitioner won't be rendered while loading so it cannot track the change from loading:true to loading:false - if (router.__store.state.status === 'pending') { - router.__store.setState((s) => ({ - ...s, - status: 'idle', - resolvedLocation: s.location, - })) - } - // hide the pending component once the load is finished - router.updateMatch(match.id, (prev) => { - return { - ...prev, - _displayPending: undefined, - displayPendingPromise: undefined, + loadPromise.then(() => { + batch(() => { + // ensure router is not in status 'pending' anymore + // this usually happens in Transitioner but if loading synchronously resolves, + // Transitioner won't be rendered while loading so it cannot track the change from loading:true to loading:false + if (router.__store.state.status === 'pending') { + router.__store.setState((s) => ({ + ...s, + status: 'idle', + resolvedLocation: s.location, + })) } + // hide the pending component once the load is finished + router.updateMatch(match.id, (prev) => { + return { + ...prev, + _displayPending: undefined, + displayPendingPromise: undefined, + } + }) }) }) - }) + } } return routeChunkPromise } + From f098befb0cd35b2d9a4efad94a96ff2edc5e1397 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Tue, 3 Mar 2026 18:32:49 +0000 Subject: [PATCH 2/3] ci: apply automated fixes --- packages/router-core/src/ssr/ssr-client.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/router-core/src/ssr/ssr-client.ts b/packages/router-core/src/ssr/ssr-client.ts index 203537d061c..c3cb8528a22 100644 --- a/packages/router-core/src/ssr/ssr-client.ts +++ b/packages/router-core/src/ssr/ssr-client.ts @@ -293,4 +293,3 @@ export async function hydrate(router: AnyRouter): Promise { } return routeChunkPromise } - From f1f10f7e83316f1e247b18e36c72ffd237331aed Mon Sep 17 00:00:00 2001 From: sleitor Date: Tue, 3 Mar 2026 20:28:59 +0000 Subject: [PATCH 3/3] fix(router-core): clear _nonReactive.displayPendingPromise on pending resolution --- packages/router-core/src/ssr/ssr-client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/router-core/src/ssr/ssr-client.ts b/packages/router-core/src/ssr/ssr-client.ts index c3cb8528a22..aedbdd43f7c 100644 --- a/packages/router-core/src/ssr/ssr-client.ts +++ b/packages/router-core/src/ssr/ssr-client.ts @@ -281,10 +281,10 @@ export async function hydrate(router: AnyRouter): Promise { } // hide the pending component once the load is finished router.updateMatch(match.id, (prev) => { + prev._nonReactive.displayPendingPromise = undefined return { ...prev, _displayPending: undefined, - displayPendingPromise: undefined, } }) })