[PR #7] [MERGED] fix: add 'use client' to next/dynamic shim for App Router RSC compatibility #240

Closed
opened 2026-05-06 12:38:43 +02:00 by BreizhHardware · 0 comments

📋 Pull Request Information

Original PR: https://github.com/cloudflare/vinext/pull/7
Author: @threepointone
Created: 2/24/2026
Status: Merged
Merged: 2/24/2026
Merged by: @southpolesteve

Base: mainHead: fix/dynamic-ssr-false-use-client


📝 Commits (1)

  • bd474c9 fix: add 'use client' to next/dynamic shim for App Router RSC compatibility

📊 Changes

6 files changed (+53 additions, -0 deletions)

View changed files

📝 packages/vinext/src/shims/dynamic.ts (+1 -0)
📝 tests/e2e/app-router/nextjs-compat/dynamic.spec.ts (+22 -0)
📝 tests/fixtures/app-basic/app/nextjs-compat/dynamic/dynamic-imports/dynamic-server.tsx (+2 -0)
📝 tests/fixtures/app-basic/app/nextjs-compat/dynamic/named-export/page.tsx (+2 -0)
tests/fixtures/app-basic/app/nextjs-compat/dynamic/ssr-false-server/client-wrapper.tsx (+12 -0)
tests/fixtures/app-basic/app/nextjs-compat/dynamic/ssr-false-server/page.tsx (+14 -0)

📄 Description

Problem

next/dynamic with ssr: false produces a blank page when used from the App Router with the Cloudflare Vite plugin. The dynamically imported component never loads on the client.

Root cause

The shims/dynamic.ts module wasn't marked as a client module. When a server component calls dynamic(), the RSC serializer executes the function on the server. For ssr: false, the server path returns null, which is serialized into the RSC payload and sent to the client as-is. The client never runs the mount-on-client code path.

In Next.js's App Router, next/dynamic resolves to a "use client" module (app-dynamic.tsx). The RSC serializer emits a client reference instead of executing the function inline.

Fix

Added "use client" to the top of packages/vinext/src/shims/dynamic.ts. This makes the dynamic shim a client module, matching Next.js behavior.

Both ssr: true and ssr: false work correctly:

  • ssr: true — SSR environment resolves the client reference and renders the component to HTML. Client hydrates. No behavior change.
  • ssr: false — SSR renders null (via isServer check). Client hydrates, useEffect fires → lazy component loads.

Test fixture updates

Existing test fixtures that called dynamic() from server modules now include "use client" — this is required since dynamic() is a client export. This matches Next.js's App Router behavior where you must call dynamic() from a client module.

Added a new E2E test (ssr:false from server component loads after hydration) with a server component page that imports a client wrapper using dynamic() with ssr: false.

All 47 test files pass (1803 tests).


🔄 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/7 **Author:** [@threepointone](https://github.com/threepointone) **Created:** 2/24/2026 **Status:** ✅ Merged **Merged:** 2/24/2026 **Merged by:** [@southpolesteve](https://github.com/southpolesteve) **Base:** `main` ← **Head:** `fix/dynamic-ssr-false-use-client` --- ### 📝 Commits (1) - [`bd474c9`](https://github.com/cloudflare/vinext/commit/bd474c9d49c8f47eca060b7b2cfa0a7886961f17) fix: add 'use client' to next/dynamic shim for App Router RSC compatibility ### 📊 Changes **6 files changed** (+53 additions, -0 deletions) <details> <summary>View changed files</summary> 📝 `packages/vinext/src/shims/dynamic.ts` (+1 -0) 📝 `tests/e2e/app-router/nextjs-compat/dynamic.spec.ts` (+22 -0) 📝 `tests/fixtures/app-basic/app/nextjs-compat/dynamic/dynamic-imports/dynamic-server.tsx` (+2 -0) 📝 `tests/fixtures/app-basic/app/nextjs-compat/dynamic/named-export/page.tsx` (+2 -0) ➕ `tests/fixtures/app-basic/app/nextjs-compat/dynamic/ssr-false-server/client-wrapper.tsx` (+12 -0) ➕ `tests/fixtures/app-basic/app/nextjs-compat/dynamic/ssr-false-server/page.tsx` (+14 -0) </details> ### 📄 Description ## Problem `next/dynamic` with `ssr: false` produces a blank page when used from the App Router with the Cloudflare Vite plugin. The dynamically imported component never loads on the client. ### Root cause The `shims/dynamic.ts` module wasn't marked as a client module. When a server component calls `dynamic()`, the RSC serializer executes the function on the server. For `ssr: false`, the server path returns `null`, which is serialized into the RSC payload and sent to the client as-is. The client never runs the mount-on-client code path. In Next.js's App Router, `next/dynamic` resolves to a `"use client"` module (`app-dynamic.tsx`). The RSC serializer emits a client reference instead of executing the function inline. ## Fix Added `"use client"` to the top of `packages/vinext/src/shims/dynamic.ts`. This makes the dynamic shim a client module, matching Next.js behavior. Both `ssr: true` and `ssr: false` work correctly: - **`ssr: true`** — SSR environment resolves the client reference and renders the component to HTML. Client hydrates. No behavior change. - **`ssr: false`** — SSR renders `null` (via `isServer` check). Client hydrates, `useEffect` fires → lazy component loads. ### Test fixture updates Existing test fixtures that called `dynamic()` from server modules now include `"use client"` — this is required since `dynamic()` is a client export. This matches Next.js's App Router behavior where you must call `dynamic()` from a client module. Added a new E2E test (`ssr:false from server component loads after hydration`) with a server component page that imports a client wrapper using `dynamic()` with `ssr: false`. All 47 test files pass (1803 tests). --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
BreizhHardware 2026-05-06 12:38:43 +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#240
No description provided.