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

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions