[PR #466] [MERGED] fix: RSC compatibility for dynamic() and layout segment context #593

Closed
opened 2026-05-06 13:08:57 +02:00 by BreizhHardware · 0 comments

📋 Pull Request Information

Original PR: https://github.com/cloudflare/vinext/pull/466
Author: @benfavre
Created: 3/11/2026
Status: Merged
Merged: 3/28/2026
Merged by: @james-elicx

Base: mainHead: fix/rsc-dynamic-layout-segment


📝 Commits (8)

  • e409b2f fix: RSC compatibility for dynamic() and layout segment context
  • 3532594 Merge remote-tracking branch 'origin/main' into fix/rsc-dynamic-layout-segment
  • 54e0793 chore: fix formatting and update entry-templates snapshots after merge with main
  • cf42681 fix: restore 'use client' in layout-segment-context to fix useSelectedLayoutSegment(s)
  • e296edf test: add regression tests for RSC dynamic() and route handler await params
  • e2e1d67 fix: address bonk review comments on dynamic.ts RSC async path
  • a410275 chore: remove unused vi import from dynamic.test.ts
  • 5bedf20 docs: correct inaccurate React.lazy/RSC comments throughout

📊 Changes

10 files changed (+207 additions, -28 deletions)

View changed files

📝 packages/vinext/src/entries/app-rsc-entry.ts (+1 -1)
📝 packages/vinext/src/shims/dynamic.ts (+50 -16)
📝 packages/vinext/src/shims/layout-segment-context.tsx (+9 -3)
📝 tests/__snapshots__/entry-templates.test.ts.snap (+6 -6)
📝 tests/dynamic.test.ts (+72 -0)
tests/fixtures/app-basic/app/nextjs-compat/dynamic/dynamic-imports/dynamic-rsc.tsx (+11 -0)
tests/fixtures/app-basic/app/nextjs-compat/dynamic/rsc-dynamic/page.tsx (+16 -0)
tests/fixtures/app-basic/app/nextjs-compat/dynamic/text-dynamic-rsc.tsx (+4 -0)
📝 tests/nextjs-compat/app-routes.test.ts (+22 -2)
📝 tests/nextjs-compat/dynamic.test.ts (+16 -0)

📄 Description

Problem

Two related issues prevent next/dynamic from working correctly in React Server Component environments, plus a missing feature for Next.js 15+ route handlers:

1. dynamic.ts crashes in RSC with React.lazy is not a function

The "use client" directive forces next/dynamic into a client component boundary, but dynamic() should work in server components too. When the RSC entry imports it, the react-server condition exports a stripped-down React that does not include useState or useEffect. The destructured imports fail silently (they become undefined). Additionally, as a defensive forward-compatibility measure, a runtime check for typeof React.lazy !== "function" is added — if a future React version strips lazy from react-server, the async server component fallback catches it (the RSC renderer natively supports async components).

2. Route handler params not wrapped as thenable

Next.js 15+ changed route handler params to be async (Promises). Route handlers that await params crash when params is a plain object from matchPattern(). The RSC entry already has makeThenableParams() for this purpose but wasn't using it for route handler params.

Note on layout-segment-context.tsx

The "use client" directive was kept in this file (it was not removed). Only the comment was updated to explain why the directive is required: LayoutSegmentProvider is imported directly by the RSC entry, and the "use client" boundary ensures it executes in the SSR/browser environment where React.createContext is available. Without it, getLayoutSegmentContext() returns null and useSelectedLayoutSegments always returns [].

Solution

dynamic.ts

  • Removed "use client" directive so dynamic() executes in the RSC module graph and can perform environment detection
  • Changed destructured imports (lazy, Suspense, useState, useEffect) to namespace access (React.lazy, React.Suspense, etc.) — these are safely checked at runtime
  • Added defensive RSC fallback: when typeof React.lazy !== "function", use an async server component pattern (the RSC renderer natively supports async components). In React 19.x, React.lazy is available in react-server so this path is forward-compatibility code.

layout-segment-context.tsx

  • Updated doc comment to explain why "use client" is required and what happens without it

app-rsc-entry.ts

  • Wrapped route handler params with makeThenableParams() so await params works correctly

Changes

File Change
packages/vinext/src/shims/dynamic.ts Remove "use client", add RSC async fallback path, use React.* namespace
packages/vinext/src/shims/layout-segment-context.tsx Update doc comment explaining why "use client" is required
packages/vinext/src/entries/app-rsc-entry.ts { params }{ params: makeThenableParams(params) }

Reproduction

// page.tsx (Server Component)
import dynamic from "next/dynamic";

// This crashed because dynamic.ts had "use client" preventing RSC environment detection
const Chart = dynamic(() => import("./Chart"), { ssr: false });

export default function Page() {
  return <Chart />;
}

// app/api/route.ts
export async function GET(request: Request, { params }: { params: Promise<{ id: string }> }) {
  const { id } = await params; // Crashed: params was a plain object, not a Promise
  return Response.json({ id });
}

Risk

Low-medium.

  • dynamic.ts: The RSC async fallback path is defensive (only activates when React.lazy is unavailable, which doesn't occur in React 19.x). SSR and client paths are functionally identical — only the import style changed from destructured to namespace.
  • layout-segment-context.tsx: Comment-only change. "use client" retained as required.
  • app-rsc-entry.ts: makeThenableParams() is already used for page params, layout params, metadata params, and slot params in the same file; this extends it to route handlers.

🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/cloudflare/vinext/pull/466 **Author:** [@benfavre](https://github.com/benfavre) **Created:** 3/11/2026 **Status:** ✅ Merged **Merged:** 3/28/2026 **Merged by:** [@james-elicx](https://github.com/james-elicx) **Base:** `main` ← **Head:** `fix/rsc-dynamic-layout-segment` --- ### 📝 Commits (8) - [`e409b2f`](https://github.com/cloudflare/vinext/commit/e409b2f99f307204bdcba8b3ba09dcb26f07f295) fix: RSC compatibility for dynamic() and layout segment context - [`3532594`](https://github.com/cloudflare/vinext/commit/35325942e96397d9fd557e69fa1671fe4770aabf) Merge remote-tracking branch 'origin/main' into fix/rsc-dynamic-layout-segment - [`54e0793`](https://github.com/cloudflare/vinext/commit/54e0793e25057cfc15bfea64f0a148cdb863bae5) chore: fix formatting and update entry-templates snapshots after merge with main - [`cf42681`](https://github.com/cloudflare/vinext/commit/cf4268148a77b9563c7e27c442e1c8716b05f1b6) fix: restore 'use client' in layout-segment-context to fix useSelectedLayoutSegment(s) - [`e296edf`](https://github.com/cloudflare/vinext/commit/e296edf7b79ca084378a438468cf46d4306fabe4) test: add regression tests for RSC dynamic() and route handler await params - [`e2e1d67`](https://github.com/cloudflare/vinext/commit/e2e1d67ff07ee4a8d90a5078f4b4f9775ccfd6bd) fix: address bonk review comments on dynamic.ts RSC async path - [`a410275`](https://github.com/cloudflare/vinext/commit/a4102759794ca06c968563affafad0a14534fb96) chore: remove unused vi import from dynamic.test.ts - [`5bedf20`](https://github.com/cloudflare/vinext/commit/5bedf20df02654b89dd7fb50823e38ce49b8abe5) docs: correct inaccurate React.lazy/RSC comments throughout ### 📊 Changes **10 files changed** (+207 additions, -28 deletions) <details> <summary>View changed files</summary> 📝 `packages/vinext/src/entries/app-rsc-entry.ts` (+1 -1) 📝 `packages/vinext/src/shims/dynamic.ts` (+50 -16) 📝 `packages/vinext/src/shims/layout-segment-context.tsx` (+9 -3) 📝 `tests/__snapshots__/entry-templates.test.ts.snap` (+6 -6) 📝 `tests/dynamic.test.ts` (+72 -0) ➕ `tests/fixtures/app-basic/app/nextjs-compat/dynamic/dynamic-imports/dynamic-rsc.tsx` (+11 -0) ➕ `tests/fixtures/app-basic/app/nextjs-compat/dynamic/rsc-dynamic/page.tsx` (+16 -0) ➕ `tests/fixtures/app-basic/app/nextjs-compat/dynamic/text-dynamic-rsc.tsx` (+4 -0) 📝 `tests/nextjs-compat/app-routes.test.ts` (+22 -2) 📝 `tests/nextjs-compat/dynamic.test.ts` (+16 -0) </details> ### 📄 Description ## Problem Two related issues prevent `next/dynamic` from working correctly in React Server Component environments, plus a missing feature for Next.js 15+ route handlers: ### 1. `dynamic.ts` crashes in RSC with `React.lazy is not a function` The `"use client"` directive forces `next/dynamic` into a client component boundary, but `dynamic()` should work in server components too. When the RSC entry imports it, the `react-server` condition exports a stripped-down React that **does not include** `useState` or `useEffect`. The destructured imports fail silently (they become `undefined`). Additionally, as a defensive forward-compatibility measure, a runtime check for `typeof React.lazy !== "function"` is added — if a future React version strips `lazy` from `react-server`, the async server component fallback catches it (the RSC renderer natively supports async components). ### 2. Route handler params not wrapped as thenable Next.js 15+ changed route handler `params` to be async (Promises). Route handlers that `await params` crash when params is a plain object from `matchPattern()`. The RSC entry already has `makeThenableParams()` for this purpose but wasn't using it for route handler params. ### Note on `layout-segment-context.tsx` The `"use client"` directive was **kept** in this file (it was not removed). Only the comment was updated to explain why the directive is required: `LayoutSegmentProvider` is imported directly by the RSC entry, and the `"use client"` boundary ensures it executes in the SSR/browser environment where `React.createContext` is available. Without it, `getLayoutSegmentContext()` returns `null` and `useSelectedLayoutSegments` always returns `[]`. ## Solution ### dynamic.ts - Removed `"use client"` directive so `dynamic()` executes in the RSC module graph and can perform environment detection - Changed destructured imports (`lazy`, `Suspense`, `useState`, `useEffect`) to namespace access (`React.lazy`, `React.Suspense`, etc.) — these are safely checked at runtime - Added defensive RSC fallback: when `typeof React.lazy !== "function"`, use an async server component pattern (the RSC renderer natively supports async components). In React 19.x, `React.lazy` is available in `react-server` so this path is forward-compatibility code. ### layout-segment-context.tsx - Updated doc comment to explain why `"use client"` is required and what happens without it ### app-rsc-entry.ts - Wrapped route handler `params` with `makeThenableParams()` so `await params` works correctly ## Changes | File | Change | |------|--------| | `packages/vinext/src/shims/dynamic.ts` | Remove `"use client"`, add RSC async fallback path, use `React.*` namespace | | `packages/vinext/src/shims/layout-segment-context.tsx` | Update doc comment explaining why `"use client"` is required | | `packages/vinext/src/entries/app-rsc-entry.ts` | `{ params }` → `{ params: makeThenableParams(params) }` | ## Reproduction ```ts // page.tsx (Server Component) import dynamic from "next/dynamic"; // This crashed because dynamic.ts had "use client" preventing RSC environment detection const Chart = dynamic(() => import("./Chart"), { ssr: false }); export default function Page() { return <Chart />; } // app/api/route.ts export async function GET(request: Request, { params }: { params: Promise<{ id: string }> }) { const { id } = await params; // Crashed: params was a plain object, not a Promise return Response.json({ id }); } ``` ## Risk Low-medium. - **dynamic.ts**: The RSC async fallback path is defensive (only activates when `React.lazy` is unavailable, which doesn't occur in React 19.x). SSR and client paths are functionally identical — only the import style changed from destructured to namespace. - **layout-segment-context.tsx**: Comment-only change. `"use client"` retained as required. - **app-rsc-entry.ts**: `makeThenableParams()` is already used for page params, layout params, metadata params, and slot params in the same file; this extends it to route handlers. --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
BreizhHardware 2026-05-06 13:08:57 +02:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/vinext#593
No description provided.