Skip to content

Commit bc43707

Browse files
authored
Fix root loader data on initial load redirects in SPA mode (#13222)
1 parent 5de9a21 commit bc43707

File tree

3 files changed

+90
-3
lines changed

3 files changed

+90
-3
lines changed

.changeset/four-ligers-search.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+
Fix root loader data on initial load redirects in SPA mode

integration/vite-spa-mode-test.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -692,6 +692,79 @@ test.describe("SPA Mode", () => {
692692
expect(await page.locator("h1").textContent()).toBe("Parent: 1");
693693
expect(await page.locator("h2").textContent()).toBe("Child");
694694
});
695+
696+
test("does not hydrate root loaderData if there's no root loader", async ({
697+
page,
698+
}) => {
699+
fixture = await createFixture({
700+
spaMode: true,
701+
files: {
702+
"react-router.config.ts": reactRouterConfig({
703+
ssr: false,
704+
splitRouteModules,
705+
}),
706+
"app/root.tsx": js`
707+
import {
708+
Meta,
709+
Links,
710+
Outlet,
711+
Routes,
712+
Route,
713+
Scripts,
714+
ScrollRestoration,
715+
} from "react-router";
716+
717+
export function Layout({ children }: { children: React.ReactNode }) {
718+
return (
719+
<html>
720+
<head>
721+
<Meta />
722+
<Links />
723+
</head>
724+
<body>
725+
{children}
726+
<ScrollRestoration />
727+
<Scripts />
728+
</body>
729+
</html>
730+
);
731+
}
732+
733+
let count = 0;
734+
export function clientLoader() {
735+
return ++count;
736+
}
737+
738+
export default function Root({ loaderData }) {
739+
return (
740+
<>
741+
<h1>{loaderData}</h1>
742+
<Outlet />
743+
</>
744+
);
745+
}
746+
`,
747+
"app/routes/_index.tsx": js`
748+
import { redirect } from 'react-router';
749+
export const clientLoader = () => redirect('/target');
750+
export default function() { return null; }
751+
`,
752+
"app/routes/target.tsx": js`
753+
import { useRouteLoaderData } from 'react-router';
754+
export default function Comp() {
755+
return <h2>{useRouteLoaderData('root')}</h2>;
756+
}
757+
`,
758+
},
759+
});
760+
appFixture = await createAppFixture(fixture);
761+
762+
let app = new PlaywrightFixture(appFixture, page);
763+
await app.goto("/");
764+
await page.waitForSelector("h2");
765+
expect(await page.locator("h1").textContent()).toBe("2");
766+
expect(await page.locator("h2").textContent()).toBe("2");
767+
});
695768
});
696769

697770
test.describe("normal apps", () => {

packages/react-router/lib/dom-export/hydrated-router.tsx

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -113,10 +113,19 @@ function createHydratedRouter({
113113

114114
let hydrationData: HydrationState | undefined = undefined;
115115
let loaderData = ssrInfo.context.state.loaderData;
116+
// In SPA mode we only hydrate build-time root loader data
116117
if (ssrInfo.context.isSpaMode) {
117-
// In SPA mode we hydrate in any build-time loader data which should be
118-
// limited to the root route
119-
hydrationData = { loaderData };
118+
if (
119+
ssrInfo.manifest.routes.root?.hasLoader &&
120+
loaderData &&
121+
"root" in loaderData
122+
) {
123+
hydrationData = {
124+
loaderData: {
125+
root: loaderData.root,
126+
},
127+
};
128+
}
120129
} else {
121130
// Create a shallow clone of `loaderData` we can mutate for partial hydration.
122131
// When a route exports a `clientLoader` and a `HydrateFallback`, the SSR will

0 commit comments

Comments
 (0)