Skip to content

Commit

Permalink
Preserve hydrated errors during partial hydration (#11305)
Browse files Browse the repository at this point in the history
  • Loading branch information
brophdawg11 committed Feb 26, 2024
1 parent 362115a commit 6b22f91
Show file tree
Hide file tree
Showing 3 changed files with 101 additions and 2 deletions.
5 changes: 5 additions & 0 deletions .changeset/partial-hydration-errors.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@remix-run/router": patch
---

Preserve hydrated errors during partial hydration runs
87 changes: 86 additions & 1 deletion packages/react-router-dom/__tests__/partial-hydration-test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import "@testing-library/jest-dom";
import { render, screen, waitFor } from "@testing-library/react";
import { act, render, screen, waitFor } from "@testing-library/react";
import * as React from "react";
import type { LoaderFunction } from "react-router";
import { RouterProvider as ReactRouter_RouterPRovider } from "react-router";
Expand Down Expand Up @@ -650,4 +650,89 @@ function testPartialHydration(
</div>"
`);
});

it("preserves hydrated errors for non-hydrating loaders", async () => {
let dfd = createDeferred();
let rootSpy: LoaderFunction = jest.fn(() => dfd.promise);
rootSpy.hydrate = true;

let indexSpy = jest.fn();

let router = createTestRouter(
[
{
id: "root",
path: "/",
loader: rootSpy,
Component() {
let data = useLoaderData() as string;
return (
<>
<h1>{`Home - ${data}`}</h1>
<Outlet />
</>
);
},
children: [
{
id: "index",
index: true,
loader: indexSpy,
Component() {
let data = useLoaderData() as string;
return <h2>{`Index - ${data}`}</h2>;
},
ErrorBoundary() {
let error = useRouteError() as string;
return <p>{error}</p>;
},
},
],
},
],
{
hydrationData: {
loaderData: {
root: "HYDRATED ROOT",
},
errors: {
index: "INDEX ERROR",
},
},
future: {
v7_partialHydration: true,
},
}
);
let { container } = render(<RouterProvider router={router} />);

expect(getHtml(container)).toMatchInlineSnapshot(`
"<div>
<h1>
Home - HYDRATED ROOT
</h1>
<p>
INDEX ERROR
</p>
</div>"
`);

expect(router.state.initialized).toBe(false);

await act(() => dfd.resolve("UPDATED ROOT"));

expect(getHtml(container)).toMatchInlineSnapshot(`
"<div>
<h1>
Home - UPDATED ROOT
</h1>
<p>
INDEX ERROR
</p>
</div>"
`);

expect(rootSpy).toHaveBeenCalledTimes(1);
expect(indexSpy).not.toHaveBeenCalled();
});
}
11 changes: 10 additions & 1 deletion packages/router/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1718,7 +1718,7 @@ export function createRouter(init: RouterInit): Router {
// preserving any new action data or existing action data (in the case of
// a revalidation interrupting an actionReload)
// If we have partialHydration enabled, then don't update the state for the
// initial data load since iot's not a "navigation"
// initial data load since it's not a "navigation"
if (
!isUninterruptedRevalidation &&
(!future.v7_partialHydration || !initialHydration)
Expand Down Expand Up @@ -1835,6 +1835,15 @@ export function createRouter(init: RouterInit): Router {
});
});

// During partial hydration, preserve SSR errors for routes that don't re-run
if (future.v7_partialHydration && initialHydration && state.errors) {
Object.entries(state.errors)
.filter(([id]) => !matchesToLoad.some((m) => m.route.id === id))
.forEach(([routeId, error]) => {
errors = Object.assign(errors || {}, { [routeId]: error });
});
}

let updatedFetchers = markFetchRedirectsDone();
let didAbortFetchLoads = abortStaleFetchLoads(pendingNavigationLoadId);
let shouldUpdateFetchers =
Expand Down

0 comments on commit 6b22f91

Please sign in to comment.