diff --git a/packages/vinext/src/entries/app-rsc-entry.ts b/packages/vinext/src/entries/app-rsc-entry.ts
index 5d43c20f7..72c404048 100644
--- a/packages/vinext/src/entries/app-rsc-entry.ts
+++ b/packages/vinext/src/entries/app-rsc-entry.ts
@@ -1957,7 +1957,7 @@ async function _handleRequest(request, __reqCtx, _mwCtx, ctx) {
if (typeof handlerFn === "function") {
const previousHeadersPhase = setHeadersAccessPhase("route-handler");
try {
- const response = await handlerFn(request, { params });
+ const response = await handlerFn(request, { params: makeThenableParams(params) });
const dynamicUsedInHandler = consumeDynamicUsage();
// Apply Cache-Control from route segment config (export const revalidate = N).
diff --git a/packages/vinext/src/shims/dynamic.ts b/packages/vinext/src/shims/dynamic.ts
index cffb35b84..8b297bbd7 100644
--- a/packages/vinext/src/shims/dynamic.ts
+++ b/packages/vinext/src/shims/dynamic.ts
@@ -1,4 +1,3 @@
-"use client";
/**
* next/dynamic shim
*
@@ -6,12 +5,17 @@
* renderToReadableStream suspends until the dynamically-imported component is
* available. On the client, also uses React.lazy for code splitting.
*
+ * Works in RSC, SSR, and client environments:
+ * - RSC: React.lazy is not available, so we use an async component pattern
+ * - SSR: React.lazy + Suspense (renderToReadableStream suspends)
+ * - Client: React.lazy + Suspense (standard code splitting)
+ *
* Supports:
* - dynamic(() => import('./Component'))
* - dynamic(() => import('./Component'), { loading: () =>
( // ssr: false — render nothing on the server, lazy-load on client if (!ssr) { if (isServer) { - // On the server, just render the loading state or nothing + // On the server (SSR or RSC), just render the loading state or nothing const SSRFalse = (_props: P) => { return LoadingComponent ? React.createElement(LoadingComponent, { isLoading: true, pastDelay: true, error: null }) @@ -101,15 +105,15 @@ function dynamic
( } // Client: use lazy with Suspense - const LazyComponent = lazy(async () => { + const LazyComponent = React.lazy(async () => { const mod = await loader(); if ("default" in mod) return mod as { default: ComponentType
}; return { default: mod as ComponentType
}; }); const ClientSSRFalse = (props: P) => { - const [mounted, setMounted] = useState(false); - useEffect(() => setMounted(true), []); + const [mounted, setMounted] = React.useState(false); + React.useEffect(() => setMounted(true), []); if (!mounted) { return LoadingComponent @@ -120,7 +124,7 @@ function dynamic
( const fallback = LoadingComponent ? React.createElement(LoadingComponent, { isLoading: true, pastDelay: true, error: null }) : null; - return React.createElement(Suspense, { fallback }, React.createElement(LazyComponent, props)); + return React.createElement(React.Suspense, { fallback }, React.createElement(LazyComponent, props)); }; ClientSSRFalse.displayName = "DynamicClientSSRFalse"; @@ -129,12 +133,25 @@ function dynamic
( // SSR-enabled path if (isServer) { - // Use React.lazy so that renderToReadableStream can suspend until the - // dynamically-imported component is available. The previous eager-load - // pattern relied on flushPreloads() being called before rendering, which - // works for the Pages Router but not the App Router where client modules - // are loaded lazily during RSC stream deserialization (issue #75). - const LazyServer = lazy(async () => { + // In RSC environment, React.lazy is not available (react-server condition + // exports a stripped-down React without lazy/useState/useEffect). + // Use an async server component pattern instead — the RSC renderer + // natively supports async components. + if (typeof React.lazy !== "function") { + const AsyncServerDynamic = async (props: P) => { + const mod = await loader(); + const Component = "default" in mod + ? (mod as { default: ComponentType
}).default + : mod as ComponentType
; + return React.createElement(Component, props); + }; + AsyncServerDynamic.displayName = "DynamicAsyncServer"; + return AsyncServerDynamic as unknown as ComponentType
; + } + + // SSR path: Use React.lazy so that renderToReadableStream can suspend + // until the dynamically-imported component is available. + const LazyServer = React.lazy(async () => { const mod = await loader(); if ("default" in mod) return mod as { default: ComponentType
}; return { default: mod as ComponentType
}; @@ -151,7 +168,7 @@ function dynamic
( const content = ErrorBoundary ? React.createElement(ErrorBoundary, { fallback: LoadingComponent }, lazyElement) : lazyElement; - return React.createElement(Suspense, { fallback }, content); + return React.createElement(React.Suspense, { fallback }, content); }; ServerDynamic.displayName = "DynamicServer"; @@ -159,7 +176,7 @@ function dynamic
( } // Client path: standard React.lazy with Suspense - const LazyComponent = lazy(async () => { + const LazyComponent = React.lazy(async () => { const mod = await loader(); if ("default" in mod) return mod as { default: ComponentType
}; return { default: mod as ComponentType
}; @@ -169,7 +186,7 @@ function dynamic
( const fallback = LoadingComponent ? React.createElement(LoadingComponent, { isLoading: true, pastDelay: true, error: null }) : null; - return React.createElement(Suspense, { fallback }, React.createElement(LazyComponent, props)); + return React.createElement(React.Suspense, { fallback }, React.createElement(LazyComponent, props)); }; ClientDynamic.displayName = "DynamicClient"; diff --git a/packages/vinext/src/shims/layout-segment-context.tsx b/packages/vinext/src/shims/layout-segment-context.tsx index b710a6277..fea9c44b5 100644 --- a/packages/vinext/src/shims/layout-segment-context.tsx +++ b/packages/vinext/src/shims/layout-segment-context.tsx @@ -1,11 +1,10 @@ -"use client"; - /** * Layout segment context provider. * - * This is a "use client" module because it needs React's createContext - * and useContext, which are NOT available in the react-server condition. - * The RSC entry renders this as a client component boundary. + * Does NOT use "use client" — this module is imported directly by the RSC + * entry which runs in the react-server condition. getLayoutSegmentContext() + * returns null when React.createContext is unavailable (RSC), and the + * provider gracefully falls back to passing children through unchanged. * * The context is shared with navigation.ts via getLayoutSegmentContext() * to avoid creating separate contexts in different modules.