diff --git a/.changeset/olive-walls-rule.md b/.changeset/olive-walls-rule.md new file mode 100644 index 0000000000..33f0e65b1f --- /dev/null +++ b/.changeset/olive-walls-rule.md @@ -0,0 +1,5 @@ +--- +"react-router": patch +--- + +Ensure `HydrateFallback` renders during SPA initialization for routes that have `middleware` but do not have a `loader` diff --git a/packages/react-router/__tests__/dom/data-browser-router-test.tsx b/packages/react-router/__tests__/dom/data-browser-router-test.tsx index a4096258d3..301faa5b63 100644 --- a/packages/react-router/__tests__/dom/data-browser-router-test.tsx +++ b/packages/react-router/__tests__/dom/data-browser-router-test.tsx @@ -335,6 +335,45 @@ function testDomRouter( `); }); + it("renders hydrateFallbackElement while first data fetch happens when it is only middleware", async () => { + let middlewareDfd = createDeferred(); + let router = createTestRouter( + [ + { + path: "/", + Component: Outlet, + HydrateFallback: () => "Loading...", + children: [ + { + path: "foo", + middleware: [() => middlewareDfd.promise], + Component: () => "Foo", + }, + ], + }, + ], + { + window: getWindow("/foo"), + }, + ); + + let { container } = render(); + + expect(getHtml(container)).toMatchInlineSnapshot(` + "
+ Loading... +
" + `); + + middlewareDfd.resolve(); + await waitFor(() => screen.getByText("Foo")); + expect(getHtml(container)).toMatchInlineSnapshot(` + "
+ Foo +
" + `); + }); + it("does not render hydrateFallback if no data fetch or lazy loading is required", async () => { let fooDefer = createDeferred(); let router = createTestRouter( diff --git a/packages/react-router/lib/hooks.tsx b/packages/react-router/lib/hooks.tsx index 4f6ffb16cb..0d4dfd1eb6 100644 --- a/packages/react-router/lib/hooks.tsx +++ b/packages/react-router/lib/hooks.tsx @@ -1159,7 +1159,7 @@ export function _renderMatches( // a given HydrateFallback while we load the rest of the hydration data let renderFallback = false; let fallbackIndex = -1; - if (dataRouterState) { + if (dataRouterState && !dataRouterState.initialized) { for (let i = 0; i < renderedMatches.length; i++) { let match = renderedMatches[i]; // Track the deepest fallback up until the first route without data @@ -1169,11 +1169,15 @@ export function _renderMatches( if (match.route.id) { let { loaderData, errors } = dataRouterState; + let needsToRunSpaMiddleware = + match.route.middleware && + match.route.middleware.length > 0 && + !match.route.loader; let needsToRunLoader = match.route.loader && !loaderData.hasOwnProperty(match.route.id) && (!errors || errors[match.route.id] === undefined); - if (match.route.lazy || needsToRunLoader) { + if (match.route.lazy || needsToRunSpaMiddleware || needsToRunLoader) { // We found the first route that's not ready to render (waiting on // lazy, or has a loader that hasn't run yet). Flag that we need to // render a fallback and render up until the appropriate fallback