Skip to content

Commit 524aed1

Browse files
committed
Fix hydrate fallback rendering for SPA middleware w/o loader
1 parent 20c3ed9 commit 524aed1

File tree

4 files changed

+65
-53
lines changed

4 files changed

+65
-53
lines changed

.changeset/olive-walls-rule.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"react-router": patch
3+
---
4+
5+
Ensure `HydrateFallback` renders during SPA initialization for routes that have `middleware` but do not have a `loader`

packages/react-router/__tests__/dom/data-browser-router-test.tsx

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -501,6 +501,60 @@ function testDomRouter(
501501
`);
502502
});
503503

504+
it("renders hydrateFallbackElement while first data fetch happens when it is only middleware", async () => {
505+
let middlewareDfd = createDeferred();
506+
let router = createTestRouter(
507+
createRoutesFromElements(
508+
<Route
509+
path="/"
510+
element={<Outlet />}
511+
hydrateFallbackElement={<FallbackElement />}
512+
>
513+
<Route
514+
path="foo"
515+
middleware={[() => middlewareDfd.promise]}
516+
element={<Foo />}
517+
/>
518+
<Route path="bar" element={<Bar />} />
519+
</Route>,
520+
),
521+
{
522+
window: getWindow("/foo"),
523+
},
524+
);
525+
let { container } = render(<RouterProvider router={router} />);
526+
527+
function FallbackElement() {
528+
return <p>Loading...</p>;
529+
}
530+
531+
function Foo() {
532+
return <h1>Foo</h1>;
533+
}
534+
535+
function Bar() {
536+
return <h1>Bar Heading</h1>;
537+
}
538+
539+
expect(getHtml(container)).toMatchInlineSnapshot(`
540+
"<div>
541+
<p>
542+
Loading...
543+
</p>
544+
</div>"
545+
`);
546+
547+
middlewareDfd.resolve();
548+
await waitFor(() => screen.getByText("Foo"));
549+
expect(getHtml(container)).toMatchInlineSnapshot(`
550+
"<div>
551+
<h1>
552+
Foo
553+
</h1>
554+
</div>"
555+
`);
556+
});
557+
504558
it("does not render fallbackElement if no data fetch or lazy loading is required", async () => {
505559
let fooDefer = createDeferred();
506560
let router = createTestRouter(

packages/react-router/__tests__/router/context-middleware-test.tsx renamed to packages/react-router/__tests__/router/context-middleware-test.ts

Lines changed: 0 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -749,57 +749,6 @@ describe("context/middleware", () => {
749749

750750
unsub();
751751
});
752-
753-
it("works with createRoutesFromElements", async () => {
754-
let context = new RouterContextProvider();
755-
router = createRouter({
756-
history: createMemoryHistory(),
757-
getContext: () => context,
758-
routes: createRoutesFromElements(
759-
<>
760-
<Route path="/" />
761-
<Route
762-
id="parent"
763-
path="/parent"
764-
middleware={[
765-
getOrderMiddleware(orderContext, "a"),
766-
getOrderMiddleware(orderContext, "b"),
767-
]}
768-
loader={({ context }) => {
769-
context.get(orderContext).push("parent loader");
770-
}}
771-
>
772-
<Route
773-
id="child"
774-
path="child"
775-
middleware={[
776-
getOrderMiddleware(orderContext, "c"),
777-
getOrderMiddleware(orderContext, "d"),
778-
]}
779-
loader={({ context }) => {
780-
context.get(orderContext).push("child loader");
781-
}}
782-
/>
783-
</Route>
784-
</>,
785-
),
786-
});
787-
788-
await router.navigate("/parent/child");
789-
790-
expect(context.get(orderContext)).toEqual([
791-
"a middleware - before next()",
792-
"b middleware - before next()",
793-
"c middleware - before next()",
794-
"d middleware - before next()",
795-
"parent loader",
796-
"child loader",
797-
"d middleware - after next()",
798-
"c middleware - after next()",
799-
"b middleware - after next()",
800-
"a middleware - after next()",
801-
]);
802-
});
803752
});
804753

805754
describe("lazy", () => {

packages/react-router/lib/hooks.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1158,7 +1158,7 @@ export function _renderMatches(
11581158
// a given HydrateFallback while we load the rest of the hydration data
11591159
let renderFallback = false;
11601160
let fallbackIndex = -1;
1161-
if (dataRouterState) {
1161+
if (dataRouterState && !dataRouterState.initialized) {
11621162
for (let i = 0; i < renderedMatches.length; i++) {
11631163
let match = renderedMatches[i];
11641164
// Track the deepest fallback up until the first route without data
@@ -1168,11 +1168,15 @@ export function _renderMatches(
11681168

11691169
if (match.route.id) {
11701170
let { loaderData, errors } = dataRouterState;
1171+
let needsToRunSpaMiddleware =
1172+
match.route.middleware &&
1173+
match.route.middleware.length > 0 &&
1174+
!match.route.loader;
11711175
let needsToRunLoader =
11721176
match.route.loader &&
11731177
!loaderData.hasOwnProperty(match.route.id) &&
11741178
(!errors || errors[match.route.id] === undefined);
1175-
if (match.route.lazy || needsToRunLoader) {
1179+
if (match.route.lazy || needsToRunSpaMiddleware || needsToRunLoader) {
11761180
// We found the first route that's not ready to render (waiting on
11771181
// lazy, or has a loader that hasn't run yet). Flag that we need to
11781182
// render a fallback and render up until the appropriate fallback

0 commit comments

Comments
 (0)