Skip to content

Commit 653d1a8

Browse files
authored
Fix hydration behavior of patchRoutesOnMiss when v7_partialHydration is enabled (#11838)
1 parent df33160 commit 653d1a8

File tree

6 files changed

+345
-9
lines changed

6 files changed

+345
-9
lines changed

.changeset/sour-dryers-walk.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"react-router-dom": patch
3+
"react-router": patch
4+
"@remix-run/router": patch
5+
---
6+
7+
Fix initial hydration behavior when using `future.v7_partialHydration` along with `unstable_patchRoutesOnMiss`
8+
9+
- During initial hydration, `router.state.matches` will now include any partial matches so that we can render ancestor `HydrateFallback` components

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -105,13 +105,13 @@
105105
},
106106
"filesize": {
107107
"packages/router/dist/router.umd.min.js": {
108-
"none": "57.1 kB"
108+
"none": "57.2 kB"
109109
},
110110
"packages/react-router/dist/react-router.production.min.js": {
111-
"none": "14.9 kB"
111+
"none": "15.0 kB"
112112
},
113113
"packages/react-router/dist/umd/react-router.production.min.js": {
114-
"none": "17.4 kB"
114+
"none": "17.5 kB"
115115
},
116116
"packages/react-router-dom/dist/react-router-dom.production.min.js": {
117117
"none": "17.3 kB"

packages/react-router-dom/__tests__/partial-hydration-test.tsx

Lines changed: 177 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import "@testing-library/jest-dom";
22
import { act, render, screen, waitFor } from "@testing-library/react";
33
import * as React from "react";
44
import type { LoaderFunction } from "react-router";
5-
import { RouterProvider as ReactRouter_RouterPRovider } from "react-router";
5+
import { RouterProvider as ReactRouter_RouterProvider } from "react-router";
66
import {
77
Outlet,
88
RouterProvider as ReactRouterDom_RouterProvider,
@@ -28,7 +28,181 @@ describe("v7_partialHydration", () => {
2828
});
2929

3030
describe("createMemoryRouter", () => {
31-
testPartialHydration(createMemoryRouter, ReactRouter_RouterPRovider);
31+
testPartialHydration(createMemoryRouter, ReactRouter_RouterProvider);
32+
33+
// these tests only run for memory since we just need to set initialEntries
34+
it("supports partial hydration w/patchRoutesOnMiss (leaf fallback)", async () => {
35+
let parentDfd = createDeferred();
36+
let childDfd = createDeferred();
37+
let router = createMemoryRouter(
38+
[
39+
{
40+
path: "/",
41+
Component() {
42+
return (
43+
<>
44+
<h1>Root</h1>
45+
<Outlet />
46+
</>
47+
);
48+
},
49+
children: [
50+
{
51+
id: "parent",
52+
path: "parent",
53+
HydrateFallback: () => <p>Parent Loading...</p>,
54+
loader: () => parentDfd.promise,
55+
Component() {
56+
let data = useLoaderData() as string;
57+
return (
58+
<>
59+
<h2>{`Parent - ${data}`}</h2>
60+
<Outlet />
61+
</>
62+
);
63+
},
64+
},
65+
],
66+
},
67+
],
68+
{
69+
future: {
70+
v7_partialHydration: true,
71+
},
72+
unstable_patchRoutesOnMiss({ path, patch }) {
73+
if (path === "/parent/child") {
74+
patch("parent", [
75+
{
76+
path: "child",
77+
loader: () => childDfd.promise,
78+
Component() {
79+
let data = useLoaderData() as string;
80+
return <h3>{`Child - ${data}`}</h3>;
81+
},
82+
},
83+
]);
84+
}
85+
},
86+
initialEntries: ["/parent/child"],
87+
}
88+
);
89+
let { container } = render(
90+
<ReactRouter_RouterProvider router={router} />
91+
);
92+
93+
parentDfd.resolve("PARENT DATA");
94+
expect(getHtml(container)).toMatchInlineSnapshot(`
95+
"<div>
96+
<h1>
97+
Root
98+
</h1>
99+
<p>
100+
Parent Loading...
101+
</p>
102+
</div>"
103+
`);
104+
105+
childDfd.resolve("CHILD DATA");
106+
await waitFor(() => screen.getByText(/CHILD DATA/));
107+
expect(getHtml(container)).toMatchInlineSnapshot(`
108+
"<div>
109+
<h1>
110+
Root
111+
</h1>
112+
<h2>
113+
Parent - PARENT DATA
114+
</h2>
115+
<h3>
116+
Child - CHILD DATA
117+
</h3>
118+
</div>"
119+
`);
120+
});
121+
122+
it("supports partial hydration w/patchRoutesOnMiss (root fallback)", async () => {
123+
let parentDfd = createDeferred();
124+
let childDfd = createDeferred();
125+
let router = createMemoryRouter(
126+
[
127+
{
128+
path: "/",
129+
HydrateFallback: () => <p>Root Loading...</p>,
130+
Component() {
131+
return (
132+
<>
133+
<h1>Root</h1>
134+
<Outlet />
135+
</>
136+
);
137+
},
138+
children: [
139+
{
140+
id: "parent",
141+
path: "parent",
142+
loader: () => parentDfd.promise,
143+
Component() {
144+
let data = useLoaderData() as string;
145+
return (
146+
<>
147+
<h2>{`Parent - ${data}`}</h2>
148+
<Outlet />
149+
</>
150+
);
151+
},
152+
},
153+
],
154+
},
155+
],
156+
{
157+
future: {
158+
v7_partialHydration: true,
159+
},
160+
unstable_patchRoutesOnMiss({ path, patch }) {
161+
if (path === "/parent/child") {
162+
patch("parent", [
163+
{
164+
path: "child",
165+
loader: () => childDfd.promise,
166+
Component() {
167+
let data = useLoaderData() as string;
168+
return <h3>{`Child - ${data}`}</h3>;
169+
},
170+
},
171+
]);
172+
}
173+
},
174+
initialEntries: ["/parent/child"],
175+
}
176+
);
177+
let { container } = render(
178+
<ReactRouter_RouterProvider router={router} />
179+
);
180+
181+
parentDfd.resolve("PARENT DATA");
182+
expect(getHtml(container)).toMatchInlineSnapshot(`
183+
"<div>
184+
<p>
185+
Root Loading...
186+
</p>
187+
</div>"
188+
`);
189+
190+
childDfd.resolve("CHILD DATA");
191+
await waitFor(() => screen.getByText(/CHILD DATA/));
192+
expect(getHtml(container)).toMatchInlineSnapshot(`
193+
"<div>
194+
<h1>
195+
Root
196+
</h1>
197+
<h2>
198+
Parent - PARENT DATA
199+
</h2>
200+
<h3>
201+
Child - CHILD DATA
202+
</h3>
203+
</div>"
204+
`);
205+
});
32206
});
33207
});
34208

@@ -39,7 +213,7 @@ function testPartialHydration(
39213
| typeof createMemoryRouter,
40214
RouterProvider:
41215
| typeof ReactRouterDom_RouterProvider
42-
| typeof ReactRouter_RouterPRovider
216+
| typeof ReactRouter_RouterProvider
43217
) {
44218
let consoleWarn: jest.SpyInstance;
45219

packages/react-router/lib/hooks.tsx

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -683,10 +683,27 @@ export function _renderMatches(
683683
future: RemixRouter["future"] | null = null
684684
): React.ReactElement | null {
685685
if (matches == null) {
686-
if (dataRouterState?.errors) {
686+
if (!dataRouterState) {
687+
return null;
688+
}
689+
690+
if (dataRouterState.errors) {
687691
// Don't bail if we have data router errors so we can render them in the
688692
// boundary. Use the pre-matched (or shimmed) matches
689693
matches = dataRouterState.matches as DataRouteMatch[];
694+
} else if (
695+
future?.v7_partialHydration &&
696+
parentMatches.length === 0 &&
697+
!dataRouterState.initialized &&
698+
dataRouterState.matches.length > 0
699+
) {
700+
// Don't bail if we're initializing with partial hydration and we have
701+
// router matches. That means we're actively running `patchRoutesOnMiss`
702+
// so we should render down the partial matches to the appropriate
703+
// `HydrateFallback`. We only do this if `parentMatches` is empty so it
704+
// only impacts the root matches for `RouterProvider` and no descendant
705+
// `<Routes>`
706+
matches = dataRouterState.matches as DataRouteMatch[];
690707
} else {
691708
return null;
692709
}

0 commit comments

Comments
 (0)