diff --git a/packages/vinext/src/shims/dynamic.ts b/packages/vinext/src/shims/dynamic.ts index 30599ff18..d3c41a841 100644 --- a/packages/vinext/src/shims/dynamic.ts +++ b/packages/vinext/src/shims/dynamic.ts @@ -74,6 +74,75 @@ function getDynamicErrorBoundary() { // Detect server vs client const isServer = typeof window === "undefined"; +/** + * Wraps non-function components (React.memo / React.forwardRef objects) into + * a plain function component so React.lazy() can consume them. + * + * React.lazy() requires `{ default: Function }`, but memo/forwardRef produce + * objects with a `$$typeof` Symbol. Without this wrapper, lazy() throws: + * "React.lazy() only accepts functions returning a module with a default export" + */ +function wrapNonFunctionComponent
(comp: unknown): ComponentType
{ + // oxlint-disable-next-line typescript/no-explicit-any + const c = comp as any; + // oxlint-disable-next-line typescript/no-explicit-any + const Wrapper = (props: P) => React.createElement(c, props as any); + Wrapper.displayName = `DynamicWrapper(${c.displayName || c.name || "Unknown"})`; + return Wrapper; +} + +/** + * Resolve a dynamically imported module to `{ default: ComponentType }`. + * + * Handles 4 cases in priority order: + * 1. `mod.default` is a function → use directly + * 2. `mod.default` is a React element type (memo/forwardRef with $$typeof) → wrap + * 3. First named export that is a function → use as default + * 4. First named export with $$typeof → wrap + * + * This covers edge cases where libraries export memo-wrapped or forwardRef-wrapped + * components as default, which React.lazy cannot consume directly. + */ +function resolveModule
(mod: Record } {
+ // Case 1: default export is a function (most common)
+ if (mod?.default && typeof mod.default === "function") {
+ return { default: mod.default as ComponentType };
+ }
+
+ // Case 2: default export is a React element type object (memo/forwardRef)
+ // oxlint-disable-next-line typescript/no-explicit-any
+ if (
+ mod?.default &&
+ typeof mod.default === "object" &&
+ (mod.default as Record (mod.default) };
+ }
+
+ // Case 3: no usable default — try first named function export
+ if (mod) {
+ const namedFns = Object.values(mod).filter(
+ (v): v is ComponentType => typeof v === "function",
+ );
+ if (namedFns.length > 0) {
+ return { default: namedFns[0] };
+ }
+
+ // Case 4: first named $$typeof export
+ const namedObjects = Object.values(mod).filter(
+ (v): v is unknown =>
+ v != null && typeof v === "object" && !!(v as Record (namedObjects[0]) };
+ }
+ }
+
+ // Fallback: preserve original behavior
+ if ("default" in mod) return mod as { default: ComponentType };
+ return { default: mod as unknown as ComponentType };
+}
+
// Legacy preload queue — kept for backward compatibility with Pages Router
// which calls flushPreloads() before rendering. The App Router uses React.lazy
// + Suspense instead, so this queue is no longer populated.
@@ -110,8 +179,7 @@ function dynamic (
// Client: use lazy with Suspense
const LazyComponent = React.lazy(async () => {
const mod = await loader();
- if ("default" in mod) return mod as { default: ComponentType };
- return { default: mod as ComponentType };
+ return resolveModule (mod as Record (
// provide loading states. Error handling also defers to the nearest
// error boundary in the component tree.
const mod = await loader();
- const Component =
- "default" in mod
- ? (mod as { default: ComponentType }).default
- : (mod as ComponentType );
- return React.createElement(Component, props);
+ const resolved = resolveModule (mod as Record (
// 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 };
+ return resolveModule (mod as Record (
// Client path: standard React.lazy with Suspense
const LazyComponent = React.lazy(async () => {
const mod = await loader();
- if ("default" in mod) return mod as { default: ComponentType };
- return { default: mod as ComponentType };
+ return resolveModule (mod as Record