Skip to content

🐛 Bug Report: vite remote's shared modules are never seeded when consumed by a webpack host (react_exports.createContext is not a function) #5002

Description

@longngo0617

Summary

When a webpack ModuleFederationPlugin host consumes a vite-built remote, the remote's shared modules are never seeded before its remoteEntry.init() evaluates, so its prebundled dep chunks throw at top-level evaluation:

[Module Federation] Failed to bridge external shared module "@emotion/react"
  TypeError: react_exports.createContext is not a function
[Module Federation] Failed to bridge external shared module "@mui/system"    (same)
[Module Federation] Failed to bridge external shared module "@mui/material"  (same)

This happens with default configuration on both sides. The error surfaces as RUNTIME-015 at the host.

Versions:

package version
@module-federation/vite (remote) 1.20.7
@module-federation/enhanced (host) 2.8.2
@module-federation/runtime 2.8.2
webpack 5.98.0
react / @mui/material / @emotion 18.3.1 / 6.5.0 / 11.11.x

Reproduction

Minimal repro (2 apps, ~10 files): https://github.com/longngo0617/mf-webpack-host-vite-remote-repro

pnpm --dir remote install && pnpm --dir host install
pnpm --dir remote dev     # vite remote  -> :5001
pnpm --dir host serve     # webpack host -> :5002

Open http://localhost:5002. Expected SUCCESS appRemote/App; actual is the error above.

Root cause as far as I traced it

1. Webpack's register() never sets lib/loaded. From the host bundle:

var register = (name, version, factory, eager) => {
  var versions = scope[name] = scope[name] || {};
  var activeVersion = versions[version];
  if(!activeVersion || (!activeVersion.loaded && (!eager != !activeVersion.eager ? eager : uniqueName > activeVersion.from)))
    versions[version] = { get: factory, from: uniqueName, eager: !!eager };
};

So a webpack-provided share is always { get, from, eager }. Observed share scope at failure time:

react: 18.3.1 from=shellHost lib=false loaded=false
__mf_module_cache__.share: {}   // empty

2. The vite plugin's seeding bails on exactly that shape. In the generated remoteEntry.js:

const pendingExternalProvider = typeof __mfGetPendingExternalSharedProvider === 'function'
  ? __mfGetPendingExternalSharedProvider(pkg, share) : undefined;
if (pendingExternalProvider && !pendingExternalProvider.lib && !pendingExternalProvider.loaded) {
  return;   // <-- react is index 0 of __mfSeedOrder
}

and __mfGetPendingExternalSharedProvider only returns a provider when isWebpackProvider(provider) is true:

function isWebpackProvider(provider) {
  if (typeof provider?.get !== 'function') return false;
  const source = Function.prototype.toString.call(provider.get);
  return source.includes('__webpack_require__');
}

Because react is first in __mfSeedOrder, __mfFirstRuntimeSeedBarrierIndex === 0, so __mfImmediateSeedKeys is empty and nothing is seeded before init.

This looks like the core defect: isWebpackProvider() being true is precisely the signal that lib/loaded will never be populated, because webpack's register() does not populate them. Yet the guard treats it the same as "provider not ready yet, wait" — the one case where waiting cannot help. Calling provider.get() and seeding when isWebpackProvider(provider) is true would seem more correct.

3. version-first guarantees the worst possible timing. In runtime-core/dist/shared/index.js (initializeSharing):

if (host.options.shareStrategy === "version-first" || strategy === "version-first")
  host.options.remotes.forEach((remote) => {
    if (remote.shareScope === shareScopeName) promises.push(initRemoteModule(remote.name));
  });

version-first must init every remote to compare versions, so the host's own first loadShare('react') triggers the remote's init() — before the host has materialised anything. I confirmed the ordering by proxying __mf_module_cache__.share: every bridge failure is logged before default:react is ever written.

4. Consequence: the workaround must be applied on both sides. The remote's strategy is baked into its artifact as a string literal, gating its own bridge:

// remote built with default version-first:
if (singleton && 'version-first' !== 'loaded-first') return;   // -> bridge compiled out
// remote built with loaded-first:
if (singleton && 'loaded-first' !== 'loaded-first') return;    // -> bridge runs

Since every share here is singleton: true, under version-first __mfBridgeMaterializedProvider is dead code in the remote — so no host-side setting can reach it.

Matrix (cold cache each run, from the repro)

host shareStrategy remote shareStrategy result
default (version-first) default (version-first) react_exports.createContext is not a function
loaded-first default styled_default is not a function + RUNTIME-015
default loaded-first react_exports.createContext is not a function
loaded-first loaded-first ✅ works

Setting shareStrategy: 'loaded-first' on both sides is the only working combination. eager: true on the host's shared deps does not help — it makes the provider's getter synchronous but still leaves lib/loaded unset, which is what the guard tests.

Notes for reproducing

Three conditions matter, and getting any of them wrong changes the failure mode:

  1. The host must consume a shared dep itself (import * as React from 'react') — its first loadShare() is what calls initializeSharing().
  2. The remote must import package indexes (@mui/material), not deep subpaths — their prebundled dep chunks call createContext() / styled() at top level.
  3. The host's uniqueName must sort above the remote's name, so the host keeps the react provider slot (see the uniqueName > activeVersion.from tiebreak in register()). With the remote sorting higher it wins the slot and the symptom becomes a deadlock — the host's own import React never resolves — rather than this throw.

Point 3 may be worth a look independently: which container ends up owning a shared module for equal versions depends on the lexicographic ordering of container names, which is a surprising thing for behaviour to hinge on.

Possibly related

#4718 looks like the same family (shared react unresolved inside a vite remote's __loadShare__ virtual module) but a different setup — vite host, React 19, useContext on null. Different cause as far as I can tell, flagging in case it helps.

Used Package Manager

pnpm

System Info

  System:
    OS: macOS 26.6.1
    CPU: (10) arm64 Apple M1 Pro
    Shell: 5.9 - /bin/zsh
  Binaries:
    Node: 24.16.0
    npm: 11.13.0
    pnpm: 11.3.0
  Browsers:
    Chrome: 151.0.7922.174
    Safari: 26.6

Validations

  • Read the docs.
  • Read the common issues list.
  • Checked that there isn't already an issue reporting the same bug.
  • This is a Module Federation issue, not a framework-specific issue.
  • The provided reproduction is a minimal reproducible example.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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