Skip to content

flattenModule uses Object.assign snapshot — breaks mutable internal state (React 19 hooks dispatcher) #741

@pedrotainha

Description

@pedrotainha

Description

flattenModule in federation_fn_import.js uses Object.assign({}, module.default, module) to merge a shared module's default export with its named exports. This creates a shallow snapshot at load time.

React 19 stores its hooks dispatcher in a mutable property:

React.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE.H

This property is null at module load time and only set during render. The Object.assign snapshot captures null permanently, so any component using React hooks via the shared module gets:

TypeError: Cannot read properties of null (reading 'useMemoCache')

or similar errors for any hook (useState, useEffect, etc.) when running with React Compiler (babel-plugin-react-compiler).

Reproduction

  • Host app shares react and react-dom
  • Remote app uses babel-plugin-react-compiler (which emits useMemoCache calls)
  • Remote loads in host → crash because hooks dispatcher is null in the snapshot

Expected behavior

Shared modules should preserve live bindings to mutable internal state.

Suggested fix

Replace Object.assign with a Proxy that delegates property access to the original module object at access time (not load time). This preserves live mutable state.

// Before (snapshot — breaks live state)
module = Object.assign({}, module.default, module)

// After (proxy — preserves live bindings)
const originalModule = module
module = new Proxy(module.default, {
  get(target, prop) {
    if (prop !== 'default' && prop in originalModule) return originalModule[prop]
    return target[prop]
  },
  has(target, prop) {
    return prop in originalModule || prop in target
  },
  ownKeys(target) {
    const keys = new Set([...Reflect.ownKeys(target), ...Reflect.ownKeys(originalModule)])
    keys.delete('default')
    return [...keys]
  }
})

Note: The originalModule reference is critical — without it, prop in module after reassignment triggers the Proxy's own has trap recursively (stack overflow).

Environment

  • vite-plugin-federation: 1.4.1
  • React: 19.x
  • babel-plugin-react-compiler: latest
  • Vite: 7.x

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions