Skip to content

Commit 1b459f5

Browse files
fix(rsc): support unstable_getContext in RSCHydratedRouter (#13960)
1 parent debdf3e commit 1b459f5

File tree

7 files changed

+123
-2
lines changed

7 files changed

+123
-2
lines changed

docs/api/rsc/RSCHydratedRouter.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,7 @@ The decoded `RSCPayload` to hydrate.
4747
### routeDiscovery
4848

4949
`eager` or `lazy` - Determines if links are eagerly discovered, or delayed until clicked.
50+
51+
### unstable_getContext
52+
53+
A function that returns an `unstable_InitialContext` object (`Map<RouterContext, unknown>`), for use in client loaders, actions and middleware.

integration/helpers/rsc-parcel/src/browser.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
setServerCallback,
1616
// @ts-expect-error - no types for this yet
1717
} from "react-server-dom-parcel/client";
18+
import { unstable_getContext } from "./config/unstable-get-context";
1819

1920
// Create and set the callServer function to support post-hydration server actions.
2021
setServerCallback(
@@ -38,6 +39,7 @@ createFromReadableStream(getRSCStream()).then((payload: RSCPayload) => {
3839
<RSCHydratedRouter
3940
payload={payload}
4041
createFromReadableStream={createFromReadableStream}
42+
unstable_getContext={unstable_getContext}
4143
/>
4244
</StrictMode>,
4345
{
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// THIS FILE IS DESIGNED TO BE OVERRIDDEN IN TESTS IF NEEDED
2+
export const unstable_getContext = undefined;
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// THIS FILE IS DESIGNED TO BE OVERRIDDEN IN TESTS IF NEEDED
2+
export const unstable_getContext = undefined;

integration/helpers/rsc-vite/src/entry.browser.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
unstable_RSCHydratedRouter as RSCHydratedRouter,
1313
} from "react-router";
1414
import type { unstable_RSCPayload as RSCPayload } from "react-router";
15+
import { unstable_getContext } from "./config/unstable-get-context";
1516

1617
setServerCallback(
1718
createCallServer({
@@ -29,6 +30,7 @@ createFromReadableStream<RSCPayload>(getRSCStream()).then((payload) => {
2930
<RSCHydratedRouter
3031
payload={payload}
3132
createFromReadableStream={createFromReadableStream}
33+
unstable_getContext={unstable_getContext}
3234
/>
3335
</StrictMode>
3436
);

integration/rsc/rsc-test.ts

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -719,6 +719,104 @@ implementations.forEach((implementation) => {
719719
validateRSCHtml(await page.content());
720720
});
721721

722+
test("Supports client context using unstable_getContext", async ({
723+
page,
724+
}) => {
725+
let port = await getPort();
726+
stop = await setupRscTest({
727+
implementation,
728+
port,
729+
files: {
730+
"src/config/unstable-get-context.ts": js`
731+
// THIS FILE OVERRIDES THE DEFAULT IMPLEMENTATION
732+
import { unstable_createContext } from "react-router";
733+
734+
export const testContext = unstable_createContext<string>("default-value");
735+
736+
export function unstable_getContext() {
737+
return new Map([[testContext, "client-context-value"]]);
738+
}
739+
`,
740+
"src/routes.ts": js`
741+
import type { unstable_RSCRouteConfig as RSCRouteConfig } from "react-router";
742+
743+
export const routes = [
744+
{
745+
id: "root",
746+
path: "",
747+
lazy: () => import("./routes/root"),
748+
children: [
749+
{
750+
id: "home",
751+
index: true,
752+
lazy: () => import("./routes/home"),
753+
},
754+
],
755+
},
756+
] satisfies RSCRouteConfig;
757+
`,
758+
"src/routes/root.tsx": js`
759+
"use client";
760+
761+
import { Outlet } from "react-router";
762+
import type { unstable_ClientMiddlewareFunction } from "react-router";
763+
import { testContext } from "../config/unstable-get-context";
764+
765+
export const unstable_clientMiddleware = [
766+
async ({ context }, next) => {
767+
context.set(testContext, "client-context-value");
768+
return await next();
769+
},
770+
];
771+
772+
export function HydrateFallback() {
773+
return <div>Loading...</div>;
774+
}
775+
776+
export default function RootRoute() {
777+
return (
778+
<div>
779+
<h1>Root Route</h1>
780+
<Outlet />
781+
</div>
782+
);
783+
}
784+
`,
785+
"src/routes/home.tsx": js`
786+
"use client";
787+
788+
import { useLoaderData } from "react-router";
789+
import { testContext } from "../config/unstable-get-context";
790+
791+
export function clientLoader({ context }) {
792+
const contextValue = context.get(testContext);
793+
return { contextValue };
794+
}
795+
796+
clientLoader.hydrate = true;
797+
798+
export default function HomeRoute() {
799+
const loaderData = useLoaderData();
800+
return (
801+
<div>
802+
<h2 data-client-context>Client context value: {loaderData.contextValue}</h2>
803+
</div>
804+
);
805+
}
806+
`,
807+
},
808+
});
809+
810+
await page.goto(`http://localhost:${port}/`);
811+
await page.waitForSelector("[data-client-context]");
812+
expect(await page.locator("[data-client-context]").textContent()).toBe(
813+
"Client context value: client-context-value"
814+
);
815+
816+
// Ensure this is using RSC
817+
validateRSCHtml(await page.content());
818+
});
819+
722820
test("Supports resource routes as URL and fetchers", async ({
723821
page,
724822
request,

packages/react-router/lib/rsc/browser.tsx

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
import { FrameworkContext } from "../dom/ssr/components";
1111
import type { FrameworkContextObject } from "../dom/ssr/entry";
1212
import { createBrowserHistory, invariant } from "../router/history";
13-
import type { Router as DataRouter } from "../router/router";
13+
import type { Router as DataRouter, RouterInit } from "../router/router";
1414
import { createRouter, isMutationMethod } from "../router/router";
1515
import type {
1616
RSCPayload,
@@ -176,11 +176,13 @@ export function createCallServer({
176176
function createRouterFromPayload({
177177
fetchImplementation,
178178
createFromReadableStream,
179+
unstable_getContext,
179180
payload,
180181
}: {
181182
payload: RSCPayload;
182183
createFromReadableStream: BrowserCreateFromReadableStreamFunction;
183184
fetchImplementation: (request: Request) => Promise<Response>;
185+
unstable_getContext: RouterInit["unstable_getContext"] | undefined;
184186
}) {
185187
if (window.__router) return window.__router;
186188

@@ -213,6 +215,7 @@ function createRouterFromPayload({
213215

214216
window.__router = createRouter({
215217
routes,
218+
unstable_getContext,
216219
basename: payload.basename,
217220
history: createBrowserHistory(),
218221
hydrationData: getHydrationData(
@@ -455,11 +458,13 @@ export function RSCHydratedRouter({
455458
fetch: fetchImplementation = fetch,
456459
payload,
457460
routeDiscovery = "eager",
461+
unstable_getContext,
458462
}: {
459463
createFromReadableStream: BrowserCreateFromReadableStreamFunction;
460464
fetch?: (request: Request) => Promise<Response>;
461465
payload: RSCPayload;
462466
routeDiscovery?: "eager" | "lazy";
467+
unstable_getContext?: RouterInit["unstable_getContext"];
463468
}) {
464469
if (payload.type !== "render") throw new Error("Invalid payload type");
465470

@@ -468,9 +473,15 @@ export function RSCHydratedRouter({
468473
createRouterFromPayload({
469474
payload,
470475
fetchImplementation,
476+
unstable_getContext,
471477
createFromReadableStream,
472478
}),
473-
[createFromReadableStream, payload, fetchImplementation]
479+
[
480+
createFromReadableStream,
481+
payload,
482+
fetchImplementation,
483+
unstable_getContext,
484+
]
474485
);
475486

476487
React.useLayoutEffect(() => {

0 commit comments

Comments
 (0)