Skip to content

Commit 669e6ee

Browse files
committed
Replace server context with AsyncLocalStorage and client context
Because [server context has been deprecated](facebook/react#27424), we needed to find a replacement for sharing the current location. Using conditional exports, we can create a universal `useRouterLocation` hook that utilizes `AsyncLocalStorage` on the server, and normal client context in the browser. Even though the client context would also be accessible in the SSR client (we could render the context provider in `ServerRoot`), we are instead using `AsyncLocalStorage` here as well, primarily for its convenience. Although this requires placing the module containing the `AsyncLocalStorage` instance into a shared webpack layer.
1 parent 603f17b commit 669e6ee

25 files changed

+187
-172
lines changed

apps/cloudflare-app/src/worker/create-rsc-app-options.tsx

Lines changed: 0 additions & 29 deletions
This file was deleted.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// This is in a separate file so that we can configure webpack to use the
2+
// `react-server` layer for this module, and therefore the imported modules
3+
// (React and the server components) will be imported with the required
4+
// `react-server` condition.
5+
6+
import {App} from '@mfng/shared-app/app.js';
7+
import * as React from 'react';
8+
9+
export function createRscApp(): React.ReactNode {
10+
return <App getTitle={(pathname) => `Cloudflare RSC/SSR demo ${pathname}`} />;
11+
}

apps/cloudflare-app/src/worker/index.ts

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1+
import {routerLocationAsyncLocalStorage} from '@mfng/core/router-location-async-local-storage';
12
import type {ServerManifest} from '@mfng/core/server/rsc';
23
import {createRscActionStream, createRscAppStream} from '@mfng/core/server/rsc';
34
import {createHtmlStream} from '@mfng/core/server/ssr';
45
import type {ClientManifest, SSRManifest} from 'react-server-dom-webpack';
5-
import {createRscAppOptions} from './create-rsc-app-options.js';
6+
import {createRscApp} from './create-rsc-app.js';
67
import type {EnvWithStaticContent, HandlerParams} from './get-json-from-kv.js';
78
import {getJsonFromKv} from './get-json-from-kv.js';
89

@@ -24,25 +25,29 @@ const handleGet: ExportedHandlerFetchHandler<EnvWithStaticContent> = async (
2425
getJsonFromKv<Record<string, string>>(`client/css-manifest.json`, params),
2526
]);
2627

27-
const rscAppStream = createRscAppStream({
28-
...createRscAppOptions({requestUrl: request.url}),
29-
reactClientManifest,
30-
mainCssHref: cssManifest[`main.css`]!,
31-
});
28+
const {pathname, search} = new URL(request.url);
3229

33-
if (request.headers.get(`accept`) === `text/x-component`) {
34-
return new Response(rscAppStream, {
35-
headers: {'Content-Type': `text/x-component; charset=utf-8`},
30+
return routerLocationAsyncLocalStorage.run({pathname, search}, async () => {
31+
const rscAppStream = createRscAppStream({
32+
app: createRscApp(),
33+
reactClientManifest,
34+
mainCssHref: cssManifest[`main.css`]!,
3635
});
37-
}
3836

39-
const htmlStream = await createHtmlStream(rscAppStream, {
40-
reactSsrManifest,
41-
bootstrapScripts: [jsManifest[`main.js`]!],
42-
});
37+
if (request.headers.get(`accept`) === `text/x-component`) {
38+
return new Response(rscAppStream, {
39+
headers: {'Content-Type': `text/x-component; charset=utf-8`},
40+
});
41+
}
42+
43+
const htmlStream = await createHtmlStream(rscAppStream, {
44+
reactSsrManifest,
45+
bootstrapScripts: [jsManifest[`main.js`]!],
46+
});
4347

44-
return new Response(htmlStream, {
45-
headers: {'Content-Type': `text/html; charset=utf-8`},
48+
return new Response(htmlStream, {
49+
headers: {'Content-Type': `text/html; charset=utf-8`},
50+
});
4651
});
4752
};
4853

apps/cloudflare-app/webpack.config.js

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,19 +95,24 @@ export default function createConfigs(_env, argv) {
9595
},
9696
resolve: {
9797
plugins: [new ResolveTypeScriptPlugin()],
98-
conditionNames: [`workerd`, `...`],
98+
conditionNames: [`workerd`, `node`, `...`],
9999
},
100100
module: {
101101
rules: [
102102
{
103103
resource: (value) =>
104104
/core\/lib\/server\/rsc\.js$/.test(value) ||
105-
/create-rsc-app-options\.tsx$/.test(value),
105+
/create-rsc-app\.tsx$/.test(value),
106106
layer: webpackRscLayerName,
107107
},
108+
{
109+
// AsyncLocalStorage module instances must be in a shared layer.
110+
layer: `shared`,
111+
test: /(router-location-async-local-storage|core\/lib\/server\/use-router-location\.js)/,
112+
},
108113
{
109114
issuerLayer: webpackRscLayerName,
110-
resolve: {conditionNames: [`react-server`, `...`]},
115+
resolve: {conditionNames: [`react-server`, `node`, `...`]},
111116
},
112117
{
113118
oneOf: [

apps/shared-app/src/client/countries-search.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
'use client';
22

33
import {useRouter} from '@mfng/core/client';
4+
import {useRouterLocation} from '@mfng/core/use-router-location';
45
import * as React from 'react';
5-
import {LocationServerContext} from '../shared/location-server-context.js';
66

77
export function CountriesSearch(): JSX.Element {
8-
const location = React.useContext(LocationServerContext);
8+
const {search} = useRouterLocation();
99
const {replace} = useRouter();
1010
const [, startTransition] = React.useTransition();
1111

1212
const [query, setQuery] = React.useState(
13-
() => new URL(location).searchParams.get(`q`) || ``,
13+
() => new URLSearchParams(search).get(`q`) || ``,
1414
);
1515

1616
const handleChange: React.ChangeEventHandler<HTMLInputElement> = (event) => {

apps/shared-app/src/server/app.tsx

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1+
import {useRouterLocation} from '@mfng/core/use-router-location';
12
import * as React from 'react';
23
import {NavigationContainer} from '../client/navigation-container.js';
3-
import {LocationServerContext} from '../shared/location-server-context.js';
44
import {Navigation} from '../shared/navigation.js';
55
import {Routes} from './routes.js';
66

@@ -9,8 +9,7 @@ export interface AppProps {
99
}
1010

1111
export function App({getTitle}: AppProps): JSX.Element {
12-
const location = React.useContext(LocationServerContext);
13-
const {pathname} = new URL(location);
12+
const {pathname} = useRouterLocation();
1413

1514
return (
1615
<html>
@@ -21,14 +20,12 @@ export function App({getTitle}: AppProps): JSX.Element {
2120
<link rel="icon" href="/client/favicon.ico" type="image/x-icon" />
2221
</head>
2322
<body>
24-
<LocationServerContext.Provider value={location}>
25-
<React.Suspense>
26-
<Navigation />
27-
<NavigationContainer>
28-
<Routes />
29-
</NavigationContainer>
30-
</React.Suspense>
31-
</LocationServerContext.Provider>
23+
<React.Suspense>
24+
<Navigation />
25+
<NavigationContainer>
26+
<Routes />
27+
</NavigationContainer>
28+
</React.Suspense>
3229
</body>
3330
</html>
3431
);

apps/shared-app/src/server/countries-list.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1+
import {useRouterLocation} from '@mfng/core/use-router-location';
12
import * as React from 'react';
2-
import {LocationServerContext} from '../shared/location-server-context.js';
33
import {countriesFuse} from './countries-fuse.js';
44

55
export function CountriesList(): JSX.Element {
6-
const location = React.useContext(LocationServerContext);
7-
const query = new URL(location).searchParams.get(`q`);
6+
const {search} = useRouterLocation();
7+
const query = new URLSearchParams(search).get(`q`);
88

99
if (!query) {
1010
return (

apps/shared-app/src/server/routes.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
1+
import {useRouterLocation} from '@mfng/core/use-router-location';
12
import * as React from 'react';
2-
import {LocationServerContext} from '../shared/location-server-context.js';
33
import {FastPage} from './fast-page.js';
44
import {HomePage} from './home-page.js';
55
import {SlowPage} from './slow-page.js';
66

77
export function Routes(): JSX.Element {
8-
const location = React.useContext(LocationServerContext);
9-
const {pathname} = new URL(location);
8+
const {pathname} = useRouterLocation();
109

1110
switch (pathname) {
1211
case `/slow-page`:

apps/shared-app/src/shared/location-server-context.ts

Lines changed: 0 additions & 8 deletions
This file was deleted.

apps/shared-app/src/shared/navigation-item.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1+
import {useRouterLocation} from '@mfng/core/use-router-location';
12
import * as React from 'react';
23
import {Link} from '../client/link.js';
3-
import {LocationServerContext} from './location-server-context.js';
44

55
export type NavigationItemProps = React.PropsWithChildren<{
66
readonly pathname: string;
@@ -10,9 +10,9 @@ export function NavigationItem({
1010
children,
1111
pathname,
1212
}: NavigationItemProps): JSX.Element {
13-
const location = React.useContext(LocationServerContext);
13+
const {pathname: currentPathname} = useRouterLocation();
1414

15-
if (pathname === new URL(location).pathname) {
15+
if (pathname === currentPathname) {
1616
return (
1717
<span className="inline-block rounded-md bg-zinc-800 py-1 px-3 text-zinc-50">
1818
{children}

apps/vercel-app/src/edge-function-handler/create-rsc-app-options.tsx

Lines changed: 0 additions & 31 deletions
This file was deleted.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// This is in a separate file so that we can configure webpack to use the
2+
// `react-server` layer for this module, and therefore the imported modules
3+
// (React and the server components) will be imported with the required
4+
// `react-server` condition.
5+
6+
import {App} from '@mfng/shared-app/app.js';
7+
import * as React from 'react';
8+
9+
export function createRscApp(): React.ReactNode {
10+
return (
11+
<App getTitle={(pathname) => `Vercel Edge RSC/SSR demo ${pathname}`} />
12+
);
13+
}

apps/vercel-app/src/edge-function-handler/index.ts

Lines changed: 28 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1+
import {routerLocationAsyncLocalStorage} from '@mfng/core/router-location-async-local-storage';
12
import {createRscActionStream, createRscAppStream} from '@mfng/core/server/rsc';
23
import {createHtmlStream} from '@mfng/core/server/ssr';
3-
import {createRscAppOptions} from './create-rsc-app-options.js';
4+
import {createRscApp} from './create-rsc-app.js';
45
import {
56
cssManifest,
67
jsManifest,
@@ -29,32 +30,37 @@ export default async function handler(request: Request): Promise<Response> {
2930

3031
const oneDay = 60 * 60 * 24;
3132

32-
async function handleGet(request: Request): Promise<Response> {
33-
const rscAppStream = createRscAppStream({
34-
...createRscAppOptions({requestUrl: request.url}),
35-
reactClientManifest,
36-
mainCssHref: cssManifest[`main.css`]!,
37-
});
33+
// eslint-disable-next-line @typescript-eslint/promise-function-async
34+
function handleGet(request: Request): Promise<Response> {
35+
const {pathname, search} = new URL(request.url);
36+
37+
return routerLocationAsyncLocalStorage.run({pathname, search}, async () => {
38+
const rscAppStream = createRscAppStream({
39+
app: createRscApp(),
40+
reactClientManifest,
41+
mainCssHref: cssManifest[`main.css`]!,
42+
});
43+
44+
if (request.headers.get(`accept`) === `text/x-component`) {
45+
return new Response(rscAppStream, {
46+
headers: {
47+
'Content-Type': `text/x-component; charset=utf-8`,
48+
'Cache-Control': `s-maxage=60, stale-while-revalidate=${oneDay}`,
49+
},
50+
});
51+
}
3852

39-
if (request.headers.get(`accept`) === `text/x-component`) {
40-
return new Response(rscAppStream, {
53+
const htmlStream = await createHtmlStream(rscAppStream, {
54+
reactSsrManifest,
55+
bootstrapScripts: [jsManifest[`main.js`]!],
56+
});
57+
58+
return new Response(htmlStream, {
4159
headers: {
42-
'Content-Type': `text/x-component; charset=utf-8`,
60+
'Content-Type': `text/html; charset=utf-8`,
4361
'Cache-Control': `s-maxage=60, stale-while-revalidate=${oneDay}`,
4462
},
4563
});
46-
}
47-
48-
const htmlStream = await createHtmlStream(rscAppStream, {
49-
reactSsrManifest,
50-
bootstrapScripts: [jsManifest[`main.js`]!],
51-
});
52-
53-
return new Response(htmlStream, {
54-
headers: {
55-
'Content-Type': `text/html; charset=utf-8`,
56-
'Cache-Control': `s-maxage=60, stale-while-revalidate=${oneDay}`,
57-
},
5864
});
5965
}
6066

apps/vercel-app/webpack.config.js

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -122,19 +122,24 @@ export default function createConfigs(_env, argv) {
122122
},
123123
resolve: {
124124
plugins: [new ResolveTypeScriptPlugin()],
125-
conditionNames: [`workerd`, `...`],
125+
conditionNames: [`workerd`, `node`, `...`],
126126
},
127127
module: {
128128
rules: [
129129
{
130130
resource: (value) =>
131131
/core\/lib\/server\/rsc\.js$/.test(value) ||
132-
/create-rsc-app-options\.tsx$/.test(value),
132+
/create-rsc-app\.tsx$/.test(value),
133133
layer: webpackRscLayerName,
134134
},
135+
{
136+
// AsyncLocalStorage module instances must be in a shared layer.
137+
layer: `shared`,
138+
test: /(router-location-async-local-storage|core\/lib\/server\/use-router-location\.js)/,
139+
},
135140
{
136141
issuerLayer: webpackRscLayerName,
137-
resolve: {conditionNames: [`react-server`, `...`]},
142+
resolve: {conditionNames: [`react-server`, `node`, `...`]},
138143
},
139144
{
140145
oneOf: [

0 commit comments

Comments
 (0)