From 8f840e71f5e9b3ccd943707fd882ef3a86ed5f08 Mon Sep 17 00:00:00 2001 From: Hendrik Liebau Date: Mon, 30 Oct 2023 20:25:55 +0100 Subject: [PATCH] Replace server context with `AsyncLocalStorage` and client context Because [server context has been deprecated](https://github.com/facebook/react/pull/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. --- .../src/worker/create-rsc-app-options.tsx | 29 ----------- .../src/worker/create-rsc-app.tsx | 11 ++++ apps/cloudflare-app/src/worker/index.ts | 37 ++++++++------ apps/cloudflare-app/webpack.config.js | 11 ++-- .../src/client/countries-search.tsx | 6 +-- apps/shared-app/src/server/app.tsx | 19 +++---- apps/shared-app/src/server/countries-list.tsx | 6 +-- apps/shared-app/src/server/routes.tsx | 5 +- .../src/shared/location-server-context.ts | 8 --- .../shared-app/src/shared/navigation-item.tsx | 6 +-- .../create-rsc-app-options.tsx | 31 ------------ .../edge-function-handler/create-rsc-app.tsx | 13 +++++ .../src/edge-function-handler/index.ts | 50 +++++++++++-------- apps/vercel-app/webpack.config.js | 11 ++-- package-lock.json | 4 +- packages/core/package.json | 9 ++++ packages/core/src/client/router-context.ts | 20 -------- packages/core/src/client/router.tsx | 9 ++-- .../core/src/client/use-router-location.ts | 20 ++++++++ packages/core/src/client/use-router.ts | 17 ++++++- .../core/src/server/create-rsc-app-stream.tsx | 11 +--- .../router-location-async-local-storage.ts | 5 ++ .../core/src/server/use-router-location.ts | 14 ++++++ packages/core/src/use-router-location.ts | 6 +++ 24 files changed, 186 insertions(+), 172 deletions(-) delete mode 100644 apps/cloudflare-app/src/worker/create-rsc-app-options.tsx create mode 100644 apps/cloudflare-app/src/worker/create-rsc-app.tsx delete mode 100644 apps/shared-app/src/shared/location-server-context.ts delete mode 100644 apps/vercel-app/src/edge-function-handler/create-rsc-app-options.tsx create mode 100644 apps/vercel-app/src/edge-function-handler/create-rsc-app.tsx delete mode 100644 packages/core/src/client/router-context.ts create mode 100644 packages/core/src/client/use-router-location.ts create mode 100644 packages/core/src/server/router-location-async-local-storage.ts create mode 100644 packages/core/src/server/use-router-location.ts create mode 100644 packages/core/src/use-router-location.ts diff --git a/apps/cloudflare-app/src/worker/create-rsc-app-options.tsx b/apps/cloudflare-app/src/worker/create-rsc-app-options.tsx deleted file mode 100644 index 5965adc..0000000 --- a/apps/cloudflare-app/src/worker/create-rsc-app-options.tsx +++ /dev/null @@ -1,29 +0,0 @@ -// This is in a separate file so that we can configure webpack to use the -// `react-server` layer for this module, and therefore the dependencies (React, -// the server components, and the server contexts) will be imported with the -// required `react-server` condition. - -import type {ServerContext} from '@mfng/core/server/rsc'; -import {App} from '@mfng/shared-app/app.js'; -import {LocationServerContextName} from '@mfng/shared-app/location-server-context.js'; -import * as React from 'react'; - -export interface CreateRscAppOptions { - readonly requestUrl: string; -} - -export interface RscAppOptions { - readonly app: React.ReactNode; - readonly serverContexts?: ServerContext[]; -} - -export function createRscAppOptions( - options: CreateRscAppOptions, -): RscAppOptions { - const {requestUrl} = options; - - return { - app: `Cloudflare RSC/SSR demo ${pathname}`} />, - serverContexts: [[LocationServerContextName, requestUrl]], - }; -} diff --git a/apps/cloudflare-app/src/worker/create-rsc-app.tsx b/apps/cloudflare-app/src/worker/create-rsc-app.tsx new file mode 100644 index 0000000..efe12ea --- /dev/null +++ b/apps/cloudflare-app/src/worker/create-rsc-app.tsx @@ -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 `Cloudflare RSC/SSR demo ${pathname}`} />; +} diff --git a/apps/cloudflare-app/src/worker/index.ts b/apps/cloudflare-app/src/worker/index.ts index 325146d..5d30e19 100644 --- a/apps/cloudflare-app/src/worker/index.ts +++ b/apps/cloudflare-app/src/worker/index.ts @@ -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'; @@ -24,25 +25,29 @@ const handleGet: ExportedHandlerFetchHandler = async ( getJsonFromKv>(`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`}, + }); }); }; diff --git a/apps/cloudflare-app/webpack.config.js b/apps/cloudflare-app/webpack.config.js index 5b7320c..95a44d6 100644 --- a/apps/cloudflare-app/webpack.config.js +++ b/apps/cloudflare-app/webpack.config.js @@ -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: [ diff --git a/apps/shared-app/src/client/countries-search.tsx b/apps/shared-app/src/client/countries-search.tsx index 8ac585c..3824328 100644 --- a/apps/shared-app/src/client/countries-search.tsx +++ b/apps/shared-app/src/client/countries-search.tsx @@ -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 = (event) => { diff --git a/apps/shared-app/src/server/app.tsx b/apps/shared-app/src/server/app.tsx index 2064d60..3e13d0d 100644 --- a/apps/shared-app/src/server/app.tsx +++ b/apps/shared-app/src/server/app.tsx @@ -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'; @@ -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 ( @@ -21,14 +20,12 @@ export function App({getTitle}: AppProps): JSX.Element { - - - - - - - - + + + + + + ); diff --git a/apps/shared-app/src/server/countries-list.tsx b/apps/shared-app/src/server/countries-list.tsx index 41ca8d6..8df0a01 100644 --- a/apps/shared-app/src/server/countries-list.tsx +++ b/apps/shared-app/src/server/countries-list.tsx @@ -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 ( diff --git a/apps/shared-app/src/server/routes.tsx b/apps/shared-app/src/server/routes.tsx index 1470954..4f1988c 100644 --- a/apps/shared-app/src/server/routes.tsx +++ b/apps/shared-app/src/server/routes.tsx @@ -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`: diff --git a/apps/shared-app/src/shared/location-server-context.ts b/apps/shared-app/src/shared/location-server-context.ts deleted file mode 100644 index 7707149..0000000 --- a/apps/shared-app/src/shared/location-server-context.ts +++ /dev/null @@ -1,8 +0,0 @@ -import * as React from 'react'; - -export const LocationServerContextName = `LocationServerContext`; - -export const LocationServerContext = React.createServerContext( - LocationServerContextName, - `/`, -); diff --git a/apps/shared-app/src/shared/navigation-item.tsx b/apps/shared-app/src/shared/navigation-item.tsx index 6f06fce..e952153 100644 --- a/apps/shared-app/src/shared/navigation-item.tsx +++ b/apps/shared-app/src/shared/navigation-item.tsx @@ -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; @@ -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 ( {children} diff --git a/apps/vercel-app/src/edge-function-handler/create-rsc-app-options.tsx b/apps/vercel-app/src/edge-function-handler/create-rsc-app-options.tsx deleted file mode 100644 index bf86090..0000000 --- a/apps/vercel-app/src/edge-function-handler/create-rsc-app-options.tsx +++ /dev/null @@ -1,31 +0,0 @@ -// This is in a separate file so that we can configure webpack to use the -// `react-server` layer for this module, and therefore the dependencies (React, -// the server components, and the server contexts) will be imported with the -// required `react-server` condition. - -import type {ServerContext} from '@mfng/core/server/rsc'; -import {App} from '@mfng/shared-app/app.js'; -import {LocationServerContextName} from '@mfng/shared-app/location-server-context.js'; -import * as React from 'react'; - -export interface CreateRscAppOptions { - readonly requestUrl: string; -} - -export interface RscAppOptions { - readonly app: React.ReactNode; - readonly serverContexts?: ServerContext[]; -} - -export function createRscAppOptions( - options: CreateRscAppOptions, -): RscAppOptions { - const {requestUrl} = options; - - return { - app: ( - `Vercel Edge RSC/SSR demo ${pathname}`} /> - ), - serverContexts: [[LocationServerContextName, requestUrl]], - }; -} diff --git a/apps/vercel-app/src/edge-function-handler/create-rsc-app.tsx b/apps/vercel-app/src/edge-function-handler/create-rsc-app.tsx new file mode 100644 index 0000000..61dee10 --- /dev/null +++ b/apps/vercel-app/src/edge-function-handler/create-rsc-app.tsx @@ -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 ( + `Vercel Edge RSC/SSR demo ${pathname}`} /> + ); +} diff --git a/apps/vercel-app/src/edge-function-handler/index.ts b/apps/vercel-app/src/edge-function-handler/index.ts index 68533c5..2b934b5 100644 --- a/apps/vercel-app/src/edge-function-handler/index.ts +++ b/apps/vercel-app/src/edge-function-handler/index.ts @@ -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, @@ -29,32 +30,37 @@ export default async function handler(request: Request): Promise { const oneDay = 60 * 60 * 24; -async function handleGet(request: Request): Promise { - 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 { + 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}`, - }, }); } diff --git a/apps/vercel-app/webpack.config.js b/apps/vercel-app/webpack.config.js index a4ecaf9..d14dbfa 100644 --- a/apps/vercel-app/webpack.config.js +++ b/apps/vercel-app/webpack.config.js @@ -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: [ diff --git a/package-lock.json b/package-lock.json index 4f550c8..002e51f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20146,7 +20146,7 @@ }, "packages/core": { "name": "@mfng/core", - "version": "1.1.0", + "version": "1.2.0", "license": "MIT", "dependencies": { "htmlescape": "^1.1.1" @@ -20167,7 +20167,7 @@ }, "packages/webpack-rsc": { "name": "@mfng/webpack-rsc", - "version": "2.2.0", + "version": "2.3.0", "license": "MIT", "dependencies": { "@babel/core": "^7.21.3", diff --git a/packages/core/package.json b/packages/core/package.json index 781accc..c641762 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -26,6 +26,15 @@ "./server/ssr": { "types": "./lib/server/ssr.d.ts", "default": "./lib/server/ssr.js" + }, + "./use-router-location": { + "types": "./lib/use-router-location.d.ts", + "node": "./lib/server/use-router-location.js", + "default": "./lib/client/use-router-location.js" + }, + "./router-location-async-local-storage": { + "types": "./lib/server/router-location-async-local-storage.d.ts", + "default": "./lib/server/router-location-async-local-storage.js" } }, "files": [ diff --git a/packages/core/src/client/router-context.ts b/packages/core/src/client/router-context.ts deleted file mode 100644 index 3c873f5..0000000 --- a/packages/core/src/client/router-context.ts +++ /dev/null @@ -1,20 +0,0 @@ -'use client'; - -import * as React from 'react'; - -export interface RouterLocation { - readonly pathname: string; - readonly search: string; -} - -export interface RouterContextValue { - readonly isPending: boolean; - readonly push: (to: Partial) => void; - readonly replace: (to: Partial) => void; -} - -export const RouterContext = React.createContext({ - isPending: false, - push: () => undefined, - replace: () => undefined, -}); diff --git a/packages/core/src/client/router.tsx b/packages/core/src/client/router.tsx index 04e2c5f..6cf75f6 100644 --- a/packages/core/src/client/router.tsx +++ b/packages/core/src/client/router.tsx @@ -1,7 +1,8 @@ import * as React from 'react'; +import type {RouterLocation} from '../use-router-location.js'; import {createFetchElementStream} from './create-fetch-element-stream.js'; -import type {RouterLocation} from './router-context.js'; -import {RouterContext} from './router-context.js'; +import {RouterLocationContext} from './use-router-location.js'; +import {RouterContext} from './use-router.js'; const fetchElementStream = createFetchElementStream( createUrlPath(document.location), @@ -82,7 +83,9 @@ export function Router(): JSX.Element { return ( - {React.use(elementStreamPromise)} + + {React.use(elementStreamPromise)} + ); } diff --git a/packages/core/src/client/use-router-location.ts b/packages/core/src/client/use-router-location.ts new file mode 100644 index 0000000..30ea6b4 --- /dev/null +++ b/packages/core/src/client/use-router-location.ts @@ -0,0 +1,20 @@ +'use client'; + +import * as React from 'react'; +import type {RouterLocation} from '../use-router-location.js'; + +export const RouterLocationContext = React.createContext< + RouterLocation | undefined +>(undefined); + +export function useRouterLocation(): RouterLocation { + const routerLocation = React.useContext(RouterLocationContext); + + if (!routerLocation) { + throw new Error( + `Called useRouterLocation() outside of a RouterLocationContext.Provider`, + ); + } + + return routerLocation; +} diff --git a/packages/core/src/client/use-router.ts b/packages/core/src/client/use-router.ts index d961df4..3286c64 100644 --- a/packages/core/src/client/use-router.ts +++ b/packages/core/src/client/use-router.ts @@ -1,6 +1,19 @@ +'use client'; + import * as React from 'react'; -import type {RouterContextValue} from './router-context.js'; -import {RouterContext} from './router-context.js'; +import type {RouterLocation} from '../use-router-location.js'; + +export interface RouterContextValue { + readonly isPending: boolean; + readonly push: (to: Partial) => void; + readonly replace: (to: Partial) => void; +} + +export const RouterContext = React.createContext({ + isPending: false, + push: () => undefined, + replace: () => undefined, +}); export function useRouter(): RouterContextValue { return React.useContext(RouterContext); diff --git a/packages/core/src/server/create-rsc-app-stream.tsx b/packages/core/src/server/create-rsc-app-stream.tsx index 8cdc6b9..0f2ddbd 100644 --- a/packages/core/src/server/create-rsc-app-stream.tsx +++ b/packages/core/src/server/create-rsc-app-stream.tsx @@ -6,15 +6,12 @@ export interface CreateRscAppStreamOptions { readonly app: React.ReactNode; readonly reactClientManifest: ClientManifest; readonly mainCssHref?: string; - readonly serverContexts?: ServerContext[]; } -export type ServerContext = [name: string, value: React.ServerContextJSONValue]; - export function createRscAppStream( options: CreateRscAppStreamOptions, ): ReadableStream { - const {app, reactClientManifest, mainCssHref, serverContexts} = options; + const {app, reactClientManifest, mainCssHref} = options; return ReactServerDOMServer.renderToReadableStream( <> @@ -29,11 +26,5 @@ export function createRscAppStream( {app} , reactClientManifest, - { - context: serverContexts && [ - [`WORKAROUND`, null], // TODO: First value has a bug where the value is not set on the second request: https://github.com/facebook/react/issues/24849 - ...serverContexts, - ], - }, ); } diff --git a/packages/core/src/server/router-location-async-local-storage.ts b/packages/core/src/server/router-location-async-local-storage.ts new file mode 100644 index 0000000..09ec103 --- /dev/null +++ b/packages/core/src/server/router-location-async-local-storage.ts @@ -0,0 +1,5 @@ +import {AsyncLocalStorage} from 'node:async_hooks'; +import type {RouterLocation} from '../use-router-location.js'; + +export const routerLocationAsyncLocalStorage = + new AsyncLocalStorage(); diff --git a/packages/core/src/server/use-router-location.ts b/packages/core/src/server/use-router-location.ts new file mode 100644 index 0000000..3184dda --- /dev/null +++ b/packages/core/src/server/use-router-location.ts @@ -0,0 +1,14 @@ +import type {RouterLocation} from '../use-router-location.js'; +import {routerLocationAsyncLocalStorage} from './router-location-async-local-storage.js'; + +export function useRouterLocation(): RouterLocation { + const routerLocation = routerLocationAsyncLocalStorage.getStore(); + + if (!routerLocation) { + throw new Error( + `useRouterLocation was called outside of an asynchronous context initialized by calling routerLocationAsyncLocalStorage.run()`, + ); + } + + return routerLocation; +} diff --git a/packages/core/src/use-router-location.ts b/packages/core/src/use-router-location.ts new file mode 100644 index 0000000..c9e375e --- /dev/null +++ b/packages/core/src/use-router-location.ts @@ -0,0 +1,6 @@ +export interface RouterLocation { + readonly pathname: string; + readonly search: string; +} + +export declare function useRouterLocation(): RouterLocation;