Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix hydration behavior of patchRoutesOnMiss when v7_partialHydration is enabled #11838

Merged
merged 4 commits into from
Jul 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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