Skip to content

Commit

Permalink
Replace server context with AsyncLocalStorage and client context
Browse files Browse the repository at this point in the history
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 does require placing the
module containing the `AsyncLocalStorage` instance into a shared webpack
layer.
  • Loading branch information
unstubbable committed Oct 31, 2023
1 parent 603f17b commit 0219e49
Show file tree
Hide file tree
Showing 25 changed files with 187 additions and 172 deletions.
29 changes: 0 additions & 29 deletions apps/cloudflare-app/src/worker/create-rsc-app-options.tsx

This file was deleted.

11 changes: 11 additions & 0 deletions apps/cloudflare-app/src/worker/create-rsc-app.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// This is in a separate file so that we can configure webpack to use the
// `react-server` layer for this module, and therefore the imported modules
// (React and the server components) will be imported with the required
// `react-server` condition.

import {App} from '@mfng/shared-app/app.js';
import * as React from 'react';

export function createRscApp(): React.ReactNode {
return <App getTitle={(pathname) => `Cloudflare RSC/SSR demo ${pathname}`} />;
}
37 changes: 21 additions & 16 deletions apps/cloudflare-app/src/worker/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import {routerLocationAsyncLocalStorage} from '@mfng/core/router-location-async-local-storage';
import type {ServerManifest} from '@mfng/core/server/rsc';
import {createRscActionStream, createRscAppStream} from '@mfng/core/server/rsc';
import {createHtmlStream} from '@mfng/core/server/ssr';
import type {ClientManifest, SSRManifest} from 'react-server-dom-webpack';
import {createRscAppOptions} from './create-rsc-app-options.js';
import {createRscApp} from './create-rsc-app.js';
import type {EnvWithStaticContent, HandlerParams} from './get-json-from-kv.js';
import {getJsonFromKv} from './get-json-from-kv.js';

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

const rscAppStream = createRscAppStream({
...createRscAppOptions({requestUrl: request.url}),
reactClientManifest,
mainCssHref: cssManifest[`main.css`]!,
});
const {pathname, search} = new URL(request.url);

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

const htmlStream = await createHtmlStream(rscAppStream, {
reactSsrManifest,
bootstrapScripts: [jsManifest[`main.js`]!],
});
if (request.headers.get(`accept`) === `text/x-component`) {
return new Response(rscAppStream, {
headers: {'Content-Type': `text/x-component; charset=utf-8`},
});
}

const htmlStream = await createHtmlStream(rscAppStream, {
reactSsrManifest,
bootstrapScripts: [jsManifest[`main.js`]!],
});

return new Response(htmlStream, {
headers: {'Content-Type': `text/html; charset=utf-8`},
return new Response(htmlStream, {
headers: {'Content-Type': `text/html; charset=utf-8`},
});
});
};

Expand Down
11 changes: 8 additions & 3 deletions apps/cloudflare-app/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,19 +95,24 @@ export default function createConfigs(_env, argv) {
},
resolve: {
plugins: [new ResolveTypeScriptPlugin()],
conditionNames: [`workerd`, `...`],
conditionNames: [`workerd`, `node`, `...`],
},
module: {
rules: [
{
resource: (value) =>
/core\/lib\/server\/rsc\.js$/.test(value) ||
/create-rsc-app-options\.tsx$/.test(value),
/create-rsc-app\.tsx$/.test(value),
layer: webpackRscLayerName,
},
{
// AsyncLocalStorage module instances must be in a shared layer.
layer: `shared`,
test: /(router-location-async-local-storage|core\/lib\/server\/use-router-location\.js)/,
},
{
issuerLayer: webpackRscLayerName,
resolve: {conditionNames: [`react-server`, `...`]},
resolve: {conditionNames: [`react-server`, `node`, `...`]},
},
{
oneOf: [
Expand Down
6 changes: 3 additions & 3 deletions apps/shared-app/src/client/countries-search.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
'use client';

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

export function CountriesSearch(): JSX.Element {
const location = React.useContext(LocationServerContext);
const {search} = useRouterLocation();
const {replace} = useRouter();
const [, startTransition] = React.useTransition();

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

const handleChange: React.ChangeEventHandler<HTMLInputElement> = (event) => {
Expand Down
19 changes: 8 additions & 11 deletions apps/shared-app/src/server/app.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {useRouterLocation} from '@mfng/core/use-router-location';
import * as React from 'react';
import {NavigationContainer} from '../client/navigation-container.js';
import {LocationServerContext} from '../shared/location-server-context.js';
import {Navigation} from '../shared/navigation.js';
import {Routes} from './routes.js';

Expand All @@ -9,8 +9,7 @@ export interface AppProps {
}

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

return (
<html>
Expand All @@ -21,14 +20,12 @@ export function App({getTitle}: AppProps): JSX.Element {
<link rel="icon" href="/client/favicon.ico" type="image/x-icon" />
</head>
<body>
<LocationServerContext.Provider value={location}>
<React.Suspense>
<Navigation />
<NavigationContainer>
<Routes />
</NavigationContainer>
</React.Suspense>
</LocationServerContext.Provider>
<React.Suspense>
<Navigation />
<NavigationContainer>
<Routes />
</NavigationContainer>
</React.Suspense>
</body>
</html>
);
Expand Down
6 changes: 3 additions & 3 deletions apps/shared-app/src/server/countries-list.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import {useRouterLocation} from '@mfng/core/use-router-location';
import * as React from 'react';
import {LocationServerContext} from '../shared/location-server-context.js';
import {countriesFuse} from './countries-fuse.js';

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

if (!query) {
return (
Expand Down
5 changes: 2 additions & 3 deletions apps/shared-app/src/server/routes.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import {useRouterLocation} from '@mfng/core/use-router-location';
import * as React from 'react';
import {LocationServerContext} from '../shared/location-server-context.js';
import {FastPage} from './fast-page.js';
import {HomePage} from './home-page.js';
import {SlowPage} from './slow-page.js';

export function Routes(): JSX.Element {
const location = React.useContext(LocationServerContext);
const {pathname} = new URL(location);
const {pathname} = useRouterLocation();

switch (pathname) {
case `/slow-page`:
Expand Down
8 changes: 0 additions & 8 deletions apps/shared-app/src/shared/location-server-context.ts

This file was deleted.

6 changes: 3 additions & 3 deletions apps/shared-app/src/shared/navigation-item.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {useRouterLocation} from '@mfng/core/use-router-location';
import * as React from 'react';
import {Link} from '../client/link.js';
import {LocationServerContext} from './location-server-context.js';

export type NavigationItemProps = React.PropsWithChildren<{
readonly pathname: string;
Expand All @@ -10,9 +10,9 @@ export function NavigationItem({
children,
pathname,
}: NavigationItemProps): JSX.Element {
const location = React.useContext(LocationServerContext);
const {pathname: currentPathname} = useRouterLocation();

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

This file was deleted.

13 changes: 13 additions & 0 deletions apps/vercel-app/src/edge-function-handler/create-rsc-app.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// This is in a separate file so that we can configure webpack to use the
// `react-server` layer for this module, and therefore the imported modules
// (React and the server components) will be imported with the required
// `react-server` condition.

import {App} from '@mfng/shared-app/app.js';
import * as React from 'react';

export function createRscApp(): React.ReactNode {
return (
<App getTitle={(pathname) => `Vercel Edge RSC/SSR demo ${pathname}`} />
);
}
50 changes: 28 additions & 22 deletions apps/vercel-app/src/edge-function-handler/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {routerLocationAsyncLocalStorage} from '@mfng/core/router-location-async-local-storage';
import {createRscActionStream, createRscAppStream} from '@mfng/core/server/rsc';
import {createHtmlStream} from '@mfng/core/server/ssr';
import {createRscAppOptions} from './create-rsc-app-options.js';
import {createRscApp} from './create-rsc-app.js';
import {
cssManifest,
jsManifest,
Expand Down Expand Up @@ -29,32 +30,37 @@ export default async function handler(request: Request): Promise<Response> {

const oneDay = 60 * 60 * 24;

async function handleGet(request: Request): Promise<Response> {
const rscAppStream = createRscAppStream({
...createRscAppOptions({requestUrl: request.url}),
reactClientManifest,
mainCssHref: cssManifest[`main.css`]!,
});
// eslint-disable-next-line @typescript-eslint/promise-function-async
function handleGet(request: Request): Promise<Response> {
const {pathname, search} = new URL(request.url);

return routerLocationAsyncLocalStorage.run({pathname, search}, async () => {
const rscAppStream = createRscAppStream({
app: createRscApp(),
reactClientManifest,
mainCssHref: cssManifest[`main.css`]!,
});

if (request.headers.get(`accept`) === `text/x-component`) {
return new Response(rscAppStream, {
headers: {
'Content-Type': `text/x-component; charset=utf-8`,
'Cache-Control': `s-maxage=60, stale-while-revalidate=${oneDay}`,
},
});
}

if (request.headers.get(`accept`) === `text/x-component`) {
return new Response(rscAppStream, {
const htmlStream = await createHtmlStream(rscAppStream, {
reactSsrManifest,
bootstrapScripts: [jsManifest[`main.js`]!],
});

return new Response(htmlStream, {
headers: {
'Content-Type': `text/x-component; charset=utf-8`,
'Content-Type': `text/html; charset=utf-8`,
'Cache-Control': `s-maxage=60, stale-while-revalidate=${oneDay}`,
},
});
}

const htmlStream = await createHtmlStream(rscAppStream, {
reactSsrManifest,
bootstrapScripts: [jsManifest[`main.js`]!],
});

return new Response(htmlStream, {
headers: {
'Content-Type': `text/html; charset=utf-8`,
'Cache-Control': `s-maxage=60, stale-while-revalidate=${oneDay}`,
},
});
}

Expand Down
11 changes: 8 additions & 3 deletions apps/vercel-app/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,19 +122,24 @@ export default function createConfigs(_env, argv) {
},
resolve: {
plugins: [new ResolveTypeScriptPlugin()],
conditionNames: [`workerd`, `...`],
conditionNames: [`workerd`, `node`, `...`],
},
module: {
rules: [
{
resource: (value) =>
/core\/lib\/server\/rsc\.js$/.test(value) ||
/create-rsc-app-options\.tsx$/.test(value),
/create-rsc-app\.tsx$/.test(value),
layer: webpackRscLayerName,
},
{
// AsyncLocalStorage module instances must be in a shared layer.
layer: `shared`,
test: /(router-location-async-local-storage|core\/lib\/server\/use-router-location\.js)/,
},
{
issuerLayer: webpackRscLayerName,
resolve: {conditionNames: [`react-server`, `...`]},
resolve: {conditionNames: [`react-server`, `node`, `...`]},
},
{
oneOf: [
Expand Down
Loading

0 comments on commit 0219e49

Please sign in to comment.