diff --git a/packages/vinext/src/entries/app-rsc-entry.ts b/packages/vinext/src/entries/app-rsc-entry.ts
index e24c79607..ca4f13888 100644
--- a/packages/vinext/src/entries/app-rsc-entry.ts
+++ b/packages/vinext/src/entries/app-rsc-entry.ts
@@ -2139,7 +2139,7 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
markDynamicUsage,
method,
middlewareContext: _mwCtx,
- params,
+ params: makeThenableParams(params),
reportRequestError: _reportRequestError,
request,
revalidateSeconds,
diff --git a/packages/vinext/src/shims/dynamic.ts b/packages/vinext/src/shims/dynamic.ts
index cffb35b84..068377048 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,19 @@
* 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: Uses React.lazy + Suspense (available in React 19.x react-server).
+ * Falls back to async component pattern if a future React version
+ * strips lazy from react-server.
+ * - 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 +107,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 +126,11 @@ 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 +139,32 @@ 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 () => {
+ // Defensive fallback: if a future React version strips React.lazy from the
+ // react-server condition, fall back to an async component pattern.
+ // In React 19.x, React.lazy IS available in react-server, so this branch
+ // does not execute — it exists for forward compatibility only.
+ if (typeof React.lazy !== "function") {
+ const AsyncServerDynamic = async (props: P) => {
+ // Note: LoadingComponent is not used here — in the RSC environment,
+ // async components suspend natively and parent }).default
+ : (mod as ComponentType );
+ return React.createElement(Component, props);
+ };
+ AsyncServerDynamic.displayName = "DynamicAsyncServer";
+ // Cast is safe: async components are natively supported by the RSC renderer,
+ // but TypeScript's ComponentType doesn't account for async return types.
+ 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 +181,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 +189,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 +199,11 @@ 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..32085cb8e 100644
--- a/packages/vinext/src/shims/layout-segment-context.tsx
+++ b/packages/vinext/src/shims/layout-segment-context.tsx
@@ -3,9 +3,15 @@
/**
* 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.
+ * Must be "use client" so that Vite's RSC bundler renders this component in
+ * the SSR/browser environment where React.createContext is available. The RSC
+ * entry imports and renders LayoutSegmentProvider directly, but because of the
+ * "use client" boundary the actual execution happens on the SSR/client side
+ * where the context can be created and consumed by useSelectedLayoutSegment(s).
+ *
+ * Without "use client", this runs in the RSC environment where
+ * React.createContext is undefined, getLayoutSegmentContext() returns null,
+ * the provider becomes a no-op, and useSelectedLayoutSegments always returns [].
*
* The context is shared with navigation.ts via getLayoutSegmentContext()
* to avoid creating separate contexts in different modules.
diff --git a/tests/__snapshots__/entry-templates.test.ts.snap b/tests/__snapshots__/entry-templates.test.ts.snap
index e27e6d3f7..fa1acb0a0 100644
--- a/tests/__snapshots__/entry-templates.test.ts.snap
+++ b/tests/__snapshots__/entry-templates.test.ts.snap
@@ -1839,7 +1839,7 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
markDynamicUsage,
method,
middlewareContext: _mwCtx,
- params,
+ params: makeThenableParams(params),
reportRequestError: _reportRequestError,
request,
revalidateSeconds,
@@ -4036,7 +4036,7 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
markDynamicUsage,
method,
middlewareContext: _mwCtx,
- params,
+ params: makeThenableParams(params),
reportRequestError: _reportRequestError,
request,
revalidateSeconds,
@@ -6239,7 +6239,7 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
markDynamicUsage,
method,
middlewareContext: _mwCtx,
- params,
+ params: makeThenableParams(params),
reportRequestError: _reportRequestError,
request,
revalidateSeconds,
@@ -8466,7 +8466,7 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
markDynamicUsage,
method,
middlewareContext: _mwCtx,
- params,
+ params: makeThenableParams(params),
reportRequestError: _reportRequestError,
request,
revalidateSeconds,
@@ -10667,7 +10667,7 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
markDynamicUsage,
method,
middlewareContext: _mwCtx,
- params,
+ params: makeThenableParams(params),
reportRequestError: _reportRequestError,
request,
revalidateSeconds,
@@ -13221,7 +13221,7 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
markDynamicUsage,
method,
middlewareContext: _mwCtx,
- params,
+ params: makeThenableParams(params),
reportRequestError: _reportRequestError,
request,
revalidateSeconds,
diff --git a/tests/dynamic.test.ts b/tests/dynamic.test.ts
index e0d11604e..5d55b3db7 100644
--- a/tests/dynamic.test.ts
+++ b/tests/dynamic.test.ts
@@ -129,3 +129,75 @@ describe("flushPreloads", () => {
expect(result).toEqual([]);
});
});
+
+// ─── RSC async component path ────────────────────────────────────────────
+//
+// React 19.x exports React.lazy from the react-server condition, so the
+// `typeof React.lazy !== "function"` guard does NOT trigger in current
+// React. The AsyncServerDynamic path is defensive forward-compatibility
+// code for hypothetical future React versions that strip lazy from RSC.
+//
+// We verify it here by temporarily stubbing React.lazy to undefined,
+// simulating the react-server environment of older or stripped React builds.
+
+describe("next/dynamic RSC async component path (React.lazy unavailable)", () => {
+ it("returns an async component (DynamicAsyncServer) when React.lazy is not a function", () => {
+ const originalLazy = React.lazy;
+ try {
+ // @ts-expect-error — simulating react-server condition where lazy is absent
+ React.lazy = undefined;
+
+ const DynamicRsc = dynamic(() => Promise.resolve({ default: Hello }));
+ expect(DynamicRsc.displayName).toBe("DynamicAsyncServer");
+ } finally {
+ React.lazy = originalLazy;
+ }
+ });
+
+ it("async component resolves and renders the dynamically loaded component", async () => {
+ const originalLazy = React.lazy;
+ try {
+ // @ts-expect-error — simulating react-server condition where lazy is absent
+ React.lazy = undefined;
+
+ const DynamicRsc = dynamic(() => Promise.resolve({ default: Hello }));
+ // The returned component is an async function — call it directly as RSC would
+ const element = await (DynamicRsc as unknown as (props: object) => Promise next-dynamic dynamic on rsc