Skip to content

Commit

Permalink
Fix hydration behavior of patchRoutesOnMiss when v7_partialHydration …
Browse files Browse the repository at this point in the history
…is enabled (#11838)
  • Loading branch information
brophdawg11 authored Jul 25, 2024
1 parent df33160 commit 653d1a8
Show file tree
Hide file tree
Showing 6 changed files with 345 additions and 9 deletions.
9 changes: 9 additions & 0 deletions .changeset/sour-dryers-walk.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"react-router-dom": patch
"react-router": patch
"@remix-run/router": patch
---

Fix initial hydration behavior when using `future.v7_partialHydration` along with `unstable_patchRoutesOnMiss`

- During initial hydration, `router.state.matches` will now include any partial matches so that we can render ancestor `HydrateFallback` components
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -105,13 +105,13 @@
},
"filesize": {
"packages/router/dist/router.umd.min.js": {
"none": "57.1 kB"
"none": "57.2 kB"
},
"packages/react-router/dist/react-router.production.min.js": {
"none": "14.9 kB"
"none": "15.0 kB"
},
"packages/react-router/dist/umd/react-router.production.min.js": {
"none": "17.4 kB"
"none": "17.5 kB"
},
"packages/react-router-dom/dist/react-router-dom.production.min.js": {
"none": "17.3 kB"
Expand Down
180 changes: 177 additions & 3 deletions packages/react-router-dom/__tests__/partial-hydration-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import "@testing-library/jest-dom";
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";
import { RouterProvider as ReactRouter_RouterProvider } from "react-router";
import {
Outlet,
RouterProvider as ReactRouterDom_RouterProvider,
Expand All @@ -28,7 +28,181 @@ describe("v7_partialHydration", () => {
});

describe("createMemoryRouter", () => {
testPartialHydration(createMemoryRouter, ReactRouter_RouterPRovider);
testPartialHydration(createMemoryRouter, ReactRouter_RouterProvider);

// these tests only run for memory since we just need to set initialEntries
it("supports partial hydration w/patchRoutesOnMiss (leaf fallback)", async () => {
let parentDfd = createDeferred();
let childDfd = createDeferred();
let router = createMemoryRouter(
[
{
path: "/",
Component() {
return (
<>
<h1>Root</h1>
<Outlet />
</>
);
},
children: [
{
id: "parent",
path: "parent",
HydrateFallback: () => <p>Parent Loading...</p>,
loader: () => parentDfd.promise,
Component() {
let data = useLoaderData() as string;
return (
<>
<h2>{`Parent - ${data}`}</h2>
<Outlet />
</>
);
},
},
],
},
],
{
future: {
v7_partialHydration: true,
},
unstable_patchRoutesOnMiss({ path, patch }) {
if (path === "/parent/child") {
patch("parent", [
{
path: "child",
loader: () => childDfd.promise,
Component() {
let data = useLoaderData() as string;
return <h3>{`Child - ${data}`}</h3>;
},
},
]);
}
},
initialEntries: ["/parent/child"],
}
);
let { container } = render(
<ReactRouter_RouterProvider router={router} />
);

parentDfd.resolve("PARENT DATA");
expect(getHtml(container)).toMatchInlineSnapshot(`
"<div>
<h1>
Root
</h1>
<p>
Parent Loading...
</p>
</div>"
`);

childDfd.resolve("CHILD DATA");
await waitFor(() => screen.getByText(/CHILD DATA/));
expect(getHtml(container)).toMatchInlineSnapshot(`
"<div>
<h1>
Root
</h1>
<h2>
Parent - PARENT DATA
</h2>
<h3>
Child - CHILD DATA
</h3>
</div>"
`);
});

it("supports partial hydration w/patchRoutesOnMiss (root fallback)", async () => {
let parentDfd = createDeferred();
let childDfd = createDeferred();
let router = createMemoryRouter(
[
{
path: "/",
HydrateFallback: () => <p>Root Loading...</p>,
Component() {
return (
<>
<h1>Root</h1>
<Outlet />
</>
);
},
children: [
{
id: "parent",
path: "parent",
loader: () => parentDfd.promise,
Component() {
let data = useLoaderData() as string;
return (
<>
<h2>{`Parent - ${data}`}</h2>
<Outlet />
</>
);
},
},
],
},
],
{
future: {
v7_partialHydration: true,
},
unstable_patchRoutesOnMiss({ path, patch }) {
if (path === "/parent/child") {
patch("parent", [
{
path: "child",
loader: () => childDfd.promise,
Component() {
let data = useLoaderData() as string;
return <h3>{`Child - ${data}`}</h3>;
},
},
]);
}
},
initialEntries: ["/parent/child"],
}
);
let { container } = render(
<ReactRouter_RouterProvider router={router} />
);

parentDfd.resolve("PARENT DATA");
expect(getHtml(container)).toMatchInlineSnapshot(`
"<div>
<p>
Root Loading...
</p>
</div>"
`);

childDfd.resolve("CHILD DATA");
await waitFor(() => screen.getByText(/CHILD DATA/));
expect(getHtml(container)).toMatchInlineSnapshot(`
"<div>
<h1>
Root
</h1>
<h2>
Parent - PARENT DATA
</h2>
<h3>
Child - CHILD DATA
</h3>
</div>"
`);
});
});
});

Expand All @@ -39,7 +213,7 @@ function testPartialHydration(
| typeof createMemoryRouter,
RouterProvider:
| typeof ReactRouterDom_RouterProvider
| typeof ReactRouter_RouterPRovider
| typeof ReactRouter_RouterProvider
) {
let consoleWarn: jest.SpyInstance;

Expand Down
19 changes: 18 additions & 1 deletion packages/react-router/lib/hooks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -683,10 +683,27 @@ export function _renderMatches(
future: RemixRouter["future"] | null = null
): React.ReactElement | null {
if (matches == null) {
if (dataRouterState?.errors) {
if (!dataRouterState) {
return null;
}

if (dataRouterState.errors) {
// Don't bail if we have data router errors so we can render them in the
// boundary. Use the pre-matched (or shimmed) matches
matches = dataRouterState.matches as DataRouteMatch[];
} else if (
future?.v7_partialHydration &&
parentMatches.length === 0 &&
!dataRouterState.initialized &&
dataRouterState.matches.length > 0
) {
// Don't bail if we're initializing with partial hydration and we have
// router matches. That means we're actively running `patchRoutesOnMiss`
// so we should render down the partial matches to the appropriate
// `HydrateFallback`. We only do this if `parentMatches` is empty so it
// only impacts the root matches for `RouterProvider` and no descendant
// `<Routes>`
matches = dataRouterState.matches as DataRouteMatch[];
} else {
return null;
}
Expand Down
Loading

0 comments on commit 653d1a8

Please sign in to comment.