From 6b22f919c5575603263d1182953c3e192c8f902c Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Mon, 26 Feb 2024 17:30:47 -0500 Subject: [PATCH] Preserve hydrated errors during partial hydration (#11305) --- .changeset/partial-hydration-errors.md | 5 ++ .../__tests__/partial-hydration-test.tsx | 87 ++++++++++++++++++- packages/router/router.ts | 11 ++- 3 files changed, 101 insertions(+), 2 deletions(-) create mode 100644 .changeset/partial-hydration-errors.md diff --git a/.changeset/partial-hydration-errors.md b/.changeset/partial-hydration-errors.md new file mode 100644 index 0000000000..1d9ea20131 --- /dev/null +++ b/.changeset/partial-hydration-errors.md @@ -0,0 +1,5 @@ +--- +"@remix-run/router": patch +--- + +Preserve hydrated errors during partial hydration runs diff --git a/packages/react-router-dom/__tests__/partial-hydration-test.tsx b/packages/react-router-dom/__tests__/partial-hydration-test.tsx index 9a2723363f..dac2f3d5d9 100644 --- a/packages/react-router-dom/__tests__/partial-hydration-test.tsx +++ b/packages/react-router-dom/__tests__/partial-hydration-test.tsx @@ -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"; @@ -650,4 +650,89 @@ function testPartialHydration( " `); }); + + 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 ( + <> +

{`Home - ${data}`}

+ + + ); + }, + children: [ + { + id: "index", + index: true, + loader: indexSpy, + Component() { + let data = useLoaderData() as string; + return

{`Index - ${data}`}

; + }, + ErrorBoundary() { + let error = useRouteError() as string; + return

{error}

; + }, + }, + ], + }, + ], + { + hydrationData: { + loaderData: { + root: "HYDRATED ROOT", + }, + errors: { + index: "INDEX ERROR", + }, + }, + future: { + v7_partialHydration: true, + }, + } + ); + let { container } = render(); + + expect(getHtml(container)).toMatchInlineSnapshot(` + "
+

+ Home - HYDRATED ROOT +

+

+ INDEX ERROR +

+
" + `); + + expect(router.state.initialized).toBe(false); + + await act(() => dfd.resolve("UPDATED ROOT")); + + expect(getHtml(container)).toMatchInlineSnapshot(` + "
+

+ Home - UPDATED ROOT +

+

+ INDEX ERROR +

+
" + `); + + expect(rootSpy).toHaveBeenCalledTimes(1); + expect(indexSpy).not.toHaveBeenCalled(); + }); } diff --git a/packages/router/router.ts b/packages/router/router.ts index 1c1b04b4aa..bdea4b0ccf 100644 --- a/packages/router/router.ts +++ b/packages/router/router.ts @@ -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) @@ -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 =