[PR #852] [CLOSED] fix(shims): handle memo/forwardRef components in next/dynamic module resolution #899

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

📋 Pull Request Information

Original PR: https://github.com/cloudflare/vinext/pull/852
Author: @467469274
Created: 4/16/2026
Status: Closed

Base: mainHead: feat/cjs-transform-import-optimization-dynamic-w8


📝 Commits (1)

  • 33715d7 fix(shims): handle memo/forwardRef in next/dynamic module resolution

📊 Changes

1 file changed (+74 additions, -11 deletions)

View changed files

📝 packages/vinext/src/shims/dynamic.ts (+74 -11)

📄 Description

Problem

React.lazy() requires { default: Function }, but React.memo() and React.forwardRef() produce objects with a $$typeof Symbol instead of plain functions.

When a library exports a memo-wrapped or forwardRef-wrapped component as its default export:

// some-library/index.ts
export default React.memo(function MyComponent() { ... })

The current "default" in mod check in dynamic.ts passes, but React.lazy fails at render time:

Element type is invalid: expected a string or class/function but got: object

This affects all 4 lazy paths: client SSR-false, async server, server lazy, and client lazy.

Solution

Added a resolveModule() function with 4-tier fallback:

  1. mod.default is a function → use directly (most common, no behavior change)
  2. mod.default is a $$typeof object (memo/forwardRef) → wrap in thin function component
  3. No usable default → try first named function export
  4. First named $$typeof export → wrap

The wrapper creates a thin function component via React.createElement(comp, props) so React.lazy() can consume it. Display names are preserved for DevTools.

Changes

  • Added wrapNonFunctionComponent() helper
  • Added resolveModule() with 4-tier resolution
  • Replaced all 4 inline "default" in mod checks with resolveModule() call
  • Zero behavior change for the common case (function default export)

Test plan

  • Tested with memo-wrapped default exports
  • Tested with forwardRef-wrapped default exports
  • Tested with standard function default exports (no behavior change)
  • Tested with named exports only (fallback path)
  • Run existing vinext test suite

🤖 Generated with Claude Code


🔄 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/852 **Author:** [@467469274](https://github.com/467469274) **Created:** 4/16/2026 **Status:** ❌ Closed **Base:** `main` ← **Head:** `feat/cjs-transform-import-optimization-dynamic-w8` --- ### 📝 Commits (1) - [`33715d7`](https://github.com/cloudflare/vinext/commit/33715d75aae523688879f43280e35bd5a66a4332) fix(shims): handle memo/forwardRef in next/dynamic module resolution ### 📊 Changes **1 file changed** (+74 additions, -11 deletions) <details> <summary>View changed files</summary> 📝 `packages/vinext/src/shims/dynamic.ts` (+74 -11) </details> ### 📄 Description ## Problem `React.lazy()` requires `{ default: Function }`, but `React.memo()` and `React.forwardRef()` produce objects with a `$$typeof` Symbol instead of plain functions. When a library exports a memo-wrapped or forwardRef-wrapped component as its default export: ```tsx // some-library/index.ts export default React.memo(function MyComponent() { ... }) ``` The current `"default" in mod` check in `dynamic.ts` passes, but React.lazy fails at render time: ``` Element type is invalid: expected a string or class/function but got: object ``` This affects all 4 lazy paths: client SSR-false, async server, server lazy, and client lazy. ## Solution Added a `resolveModule()` function with 4-tier fallback: 1. `mod.default` is a function → use directly (most common, no behavior change) 2. `mod.default` is a `$$typeof` object (memo/forwardRef) → wrap in thin function component 3. No usable default → try first named function export 4. First named `$$typeof` export → wrap The wrapper creates a thin function component via `React.createElement(comp, props)` so `React.lazy()` can consume it. Display names are preserved for DevTools. ## Changes - Added `wrapNonFunctionComponent()` helper - Added `resolveModule()` with 4-tier resolution - Replaced all 4 inline `"default" in mod` checks with `resolveModule()` call - Zero behavior change for the common case (function default export) ## Test plan - [x] Tested with memo-wrapped default exports - [x] Tested with forwardRef-wrapped default exports - [x] Tested with standard function default exports (no behavior change) - [x] Tested with named exports only (fallback path) - [ ] Run existing vinext test suite 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
BreizhHardware 2026-05-06 13:10: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#899
No description provided.