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: () => }) * - dynamic(() => import('./Component'), { ssr: false }) */ -import React, { lazy, Suspense, type ComponentType, useState, useEffect } from "react"; +import React, { type ComponentType } from "react"; interface DynamicOptions { loading?: ComponentType<{ error?: Error | null; isLoading?: boolean; pastDelay?: boolean }>; @@ -90,7 +94,7 @@ function dynamic

( // 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.