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