Skip to content

Commit

Permalink
Don't run loaders below the boundary during partial hydration (#11324)
Browse files Browse the repository at this point in the history
  • Loading branch information
brophdawg11 authored Mar 5, 2024
1 parent 98e7f7b commit 05588d2
Show file tree
Hide file tree
Showing 3 changed files with 78 additions and 8 deletions.
5 changes: 5 additions & 0 deletions .changeset/partial-hydration-bubbled-error.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@remix-run/router": patch
---

Fix a `future.v7_partialHydration` bug that would re-run loaders below the boundary on hydration if SSR loader errors bubbled to a parent boundary
59 changes: 58 additions & 1 deletion packages/router/__tests__/route-fallback-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -401,7 +401,7 @@ describe("future.v7_partialHydration", () => {
});
});

it("does not kick off initial data load if errors exist", async () => {
it("does not kick off initial data load if errors exist (parent error)", async () => {
let consoleWarnSpy = jest
.spyOn(console, "warn")
.mockImplementation(() => {});
Expand Down Expand Up @@ -457,5 +457,62 @@ describe("future.v7_partialHydration", () => {
router.dispose();
consoleWarnSpy.mockReset();
});

it("does not kick off initial data load if errors exist (bubbled child error)", async () => {
let consoleWarnSpy = jest
.spyOn(console, "warn")
.mockImplementation(() => {});
let parentDfd = createDeferred();
let parentSpy = jest.fn(() => parentDfd.promise);
let childDfd = createDeferred();
let childSpy = jest.fn(() => childDfd.promise);
let router = createRouter({
history: createMemoryHistory({ initialEntries: ["/child"] }),
routes: [
{
path: "/",
loader: parentSpy,
children: [
{
path: "child",
loader: childSpy,
},
],
},
],
future: {
v7_partialHydration: true,
},
hydrationData: {
errors: {
"0": "CHILD ERROR",
},
loaderData: {
"0": "PARENT DATA",
},
},
});
router.initialize();

expect(consoleWarnSpy).not.toHaveBeenCalled();
expect(parentSpy).not.toHaveBeenCalled();
expect(childSpy).not.toHaveBeenCalled();
expect(router.state).toMatchObject({
historyAction: "POP",
location: expect.objectContaining({ pathname: "/child" }),
matches: [{ route: { path: "/" } }, { route: { path: "child" } }],
initialized: true,
navigation: IDLE_NAVIGATION,
errors: {
"0": "CHILD ERROR",
},
loaderData: {
"0": "PARENT DATA",
},
});

router.dispose();
consoleWarnSpy.mockReset();
});
});
});
22 changes: 15 additions & 7 deletions packages/router/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -833,13 +833,21 @@ export function createRouter(init: RouterInit): Router {
// were marked for explicit hydration
let loaderData = init.hydrationData ? init.hydrationData.loaderData : null;
let errors = init.hydrationData ? init.hydrationData.errors : null;
initialized = initialMatches.every(
(m) =>
m.route.loader &&
m.route.loader.hydrate !== true &&
((loaderData && loaderData[m.route.id] !== undefined) ||
(errors && errors[m.route.id] !== undefined))
);
let isRouteInitialized = (m: AgnosticDataRouteMatch) =>
m.route.loader &&
m.route.loader.hydrate !== true &&
((loaderData && loaderData[m.route.id] !== undefined) ||
(errors && errors[m.route.id] !== undefined));

// If errors exist, don't consider routes below the boundary
if (errors) {
let idx = initialMatches.findIndex(
(m) => errors![m.route.id] !== undefined
);
initialized = initialMatches.slice(0, idx + 1).every(isRouteInitialized);
} else {
initialized = initialMatches.every(isRouteInitialized);
}
} else {
// Without partial hydration - we're initialized if we were provided any
// hydrationData - which is expected to be complete
Expand Down

0 comments on commit 05588d2

Please sign in to comment.