Skip to content

Commit 501d554

Browse files
authored
Handle redirects from hydrating clientLoaders (#13477)
1 parent 2a128f1 commit 501d554

File tree

3 files changed

+91
-2
lines changed

3 files changed

+91
-2
lines changed

.changeset/smooth-donuts-bake.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"react-router": patch
3+
---
4+
5+
Handle redirects from `clientLoader.hydrate` initial load executions

integration/client-data-test.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -909,6 +909,85 @@ test.describe("Client Data", () => {
909909
expect(logs).toEqual(["running parent client loader"]);
910910
console.error = _consoleError;
911911
});
912+
913+
test("hydrating clientLoader redirects trigger new .data requests to the server", async ({
914+
page,
915+
}) => {
916+
appFixture = await createAppFixture(
917+
await createFixture({
918+
files: {
919+
"react-router.config.ts": reactRouterConfig({
920+
splitRouteModules,
921+
}),
922+
"app/root.tsx": js`
923+
import { Outlet, Scripts } from "react-router"
924+
925+
let count = 1;
926+
export function loader() {
927+
return count++;
928+
}
929+
930+
export default function Root({ loaderData }) {
931+
return (
932+
<html>
933+
<head></head>
934+
<body>
935+
<main>
936+
<p id="root-data">{loaderData}</p>
937+
<Outlet />
938+
</main>
939+
<Scripts />
940+
</body>
941+
</html>
942+
);
943+
}
944+
`,
945+
"app/routes/parent.tsx": js`
946+
import { Outlet } from 'react-router'
947+
let count = 1;
948+
export function loader() {
949+
return count++;
950+
}
951+
export default function Component({ loaderData }) {
952+
return (
953+
<>
954+
<p id="parent-data">{loaderData}</p>
955+
<Outlet/>
956+
</>
957+
);
958+
}
959+
export function shouldRevalidate() {
960+
return false;
961+
}
962+
`,
963+
"app/routes/parent.a.tsx": js`
964+
import { redirect } from 'react-router'
965+
export function clientLoader() {
966+
return redirect('/parent/b');
967+
}
968+
clientLoader.hydrate = true;
969+
export default function Component({ loaderData }) {
970+
return <p>Should not see me</p>;
971+
}
972+
`,
973+
"app/routes/parent.b.tsx": js`
974+
export default function Component({ loaderData }) {
975+
return <p id="b">Hi!</p>;
976+
}
977+
`,
978+
},
979+
})
980+
);
981+
let app = new PlaywrightFixture(appFixture, page);
982+
983+
await app.goto("/parent/a");
984+
await page.waitForSelector("#b");
985+
// Root re-runs
986+
await expect(page.locator("#root-data")).toHaveText("2");
987+
// But parent opted out of revalidation
988+
await expect(page.locator("#parent-data")).toHaveText("1");
989+
await expect(page.locator("#b")).toHaveText("Hi!");
990+
});
912991
});
913992

914993
test.describe("clientLoader - lazy route module", () => {

packages/react-router/lib/dom/ssr/single-fetch.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -428,14 +428,19 @@ async function singleFetchLoaderNavigationStrategy(
428428
await Promise.all(routeDfds.map((d) => d.promise));
429429

430430
// We can skip the server call:
431-
// - On initial hydration - only clientLoaders can pass through via `clientLoader.hydrate`
431+
// - On initial hydration - only clientLoaders can pass through via
432+
// `clientLoader.hydrate`. We check the navigation state below as well
433+
// because if a clientLoader redirected we'll still be `initialized=false`
434+
// but we want to call loaders for the new location
432435
// - If there are no routes to fetch from the server
433436
//
434437
// One exception - if we are performing an HDR revalidation we have to call
435438
// the server in case a new loader has shown up that the manifest doesn't yet
436439
// know about
440+
let isInitialLoad =
441+
!router.state.initialized && router.state.navigation.state === "idle";
437442
if (
438-
(!router.state.initialized || routesParams.size === 0) &&
443+
(isInitialLoad || routesParams.size === 0) &&
439444
!window.__reactRouterHdrActive
440445
) {
441446
singleFetchDfd.resolve({ routes: {} });

0 commit comments

Comments
 (0)