What you'll learn: By the end of this lesson you will be able to explain how Module Federation lets one React application load modules that another application builds and deploys, read a configuration made of
name,exposes,remotesandshared, say why React and React DOM are shared as singletons, trace what happens from a host's first request for a remote module to a rendered component, and name the three layers that keep a failing remote from taking the page down.
A city library system lets you order a book that sits in another branch. You ask at your own desk, the book arrives when you need it, and you read it without owning a copy. For this to work, the branches agree on a catalogue (what can be borrowed), a way to ask (where to look), and a few shared reference works so that nobody has two different editions of the dictionary on the same table. If another branch is closed, your request fails, and a good library has a plan for that.
Module Federation is that arrangement for JavaScript. One application, the remote, builds some of its modules so that others can borrow them. Another, the host, asks for them while the page runs. The running example is the grocery storefront from the previous lessons: a shell application (the host) that loads a Checkout panel built and deployed by another team (the remote), both written in React.
What was and was not run
This lesson has no lab. A real demonstration needs a bundler build of at least two applications served from two addresses, and nothing in this lesson was built or run. Every configuration shown here is made only of options documented on the Module Federation pages listed under Official documentation, which were read on 2026-11-08, and the places where several documented fragments were assembled into one file are marked. Treat the snippets as reading material, check them against the current documentation for your bundler and version, and run them in a project of your own.
Version facts at the time of writing: the documentation distinguishes Module Federation 2.0 from the Module Federation built into Webpack 5; it lists Rspack and Webpack among the supported bundlers, and its quick start says the build plugins need Node.js 20 or higher. The npm registry listed @module-federation/enhanced at version 2.9.3 as its latest release. The documentation's 2.0 stable announcement says the upgrade contains no breaking changes and lists two small ones (the default library.type changed from var to global, and runtimePlugins now accept configuration parameters). Do not copy a version number from this lesson into a project: read the registry the day you start.
The core idea
Module Federation separates "who builds a module" from "who uses it", and joins them at run time. Its documentation describes it as an architectural pattern for splitting JavaScript applications into parts, like microservices on the server. Three words carry the whole idea:
- Expose. A producer (also called a remote or provider) lists modules that other applications may load, with the
exposessetting. - Consume. A consumer (also called a host) lists the producers it loads from, with the
remotessetting. The documentation notes that one application can be both. - Share. Both sides list dependencies, such as React, that should be loaded once and reused, with the
sharedsetting.
The previous lessons covered whether to split a front end, Monolith vs Micro-Frontends, and where the join can happen, Micro-Frontend Composition. This lesson is the run-time, bundler-based join in detail. Planned follow-ups cover independent deployment and version strategy for shared dependencies, so they are only named here.
Three illustrations run through the lesson: the minimal example is a pair of configuration files, the realistic example is the storefront's Checkout remote in "In practice", and the failure example is a remote that is unreachable when the host starts.
Figure 1. A host, a remote and what they negotiate
How to read this: the host knows only a name and an address for the remote. The remote publishes what it exposes. Both sides agree to use a single copy of React, which is what the shared setting negotiates.
The configuration, piece by piece
The configuration is a set of options handed to a build plugin. All of the following come from the documentation's configuration pages.
| Option | What the documentation says | Notes |
|---|---|---|
name |
Required string; the name of the Module Federation module, which "must be unique". It is used to retrieve data at runtime and to reference global chunk storage variables. | The only stated rule is uniqueness. |
filename |
Optional; the name of the generated remoteEntry file. Default remoteEntry.js. |
The entry script that gives access to the exposed modules. |
exposes |
Optional; an object whose keys name the exposed entries and whose values are file paths. Setting it marks the build as a producer. Exposed modules go into a separate chunk. | Keys follow Node.js package entry conventions but do not support regular expressions. |
remotes |
Optional; an object from a local alias to a string of the form name@entry. Setting it marks the build as a consumer. |
The entry is either mf-manifest.json or remoteEntry.js. |
shared |
Optional; the dependencies to share between producers and consumers to reduce download size. | An array of names, or an object of per-dependency options. |
shareScope |
Optional; the share scope or scopes, default 'default'. |
Lets some shared dependencies be isolated from the default pool. |
For remotes, the documentation says the alias is the local name you import from: with alias demo you write import xx from 'demo'. The name is the producer's Module Federation name. The page also says the manifest entry, mf-manifest.json, adds dynamic module type hints, resource preloading and Chrome devtools debugging support over the plain remoteEntry.js.
Minimal example: a producer and a consumer
The two files below are assembled from fragments the documentation shows: the names provider and host, the ./Button expose key with a path of the form ./src/components/Button.tsx, the provider@http://localhost:2004/mf-manifest.json remote entry, and the singleton: true shared setting for react and react-dom. They were not run.
// module-federation.config.js in the producer
module.exports = {
name: 'provider',
exposes: {
'./Button': './src/components/Button.tsx',
},
shared: {
react: { singleton: true },
'react-dom': { singleton: true },
},
};
// module-federation.config.js in the consumer
module.exports = {
name: 'host',
remotes: {
provider: 'provider@http://localhost:2004/mf-manifest.json',
},
shared: {
react: { singleton: true },
'react-dom': { singleton: true },
},
};
For Webpack, the documentation installs the @module-federation/enhanced package, imports the plugin from its Webpack entry point and passes the config to it in the plugins array:
// webpack.config.js (excerpt: entry, output and the rest of a real config are omitted)
const { ModuleFederationPlugin } = require('@module-federation/enhanced/webpack');
const mfConfig = require('./module-federation.config');
module.exports = {
plugins: [new ModuleFederationPlugin(mfConfig)],
};
The documentation's own example also sets a dev server port and an output.publicPath that matches the address, and that matters: a remote's files must be reachable at the address the manifest and the remotes entry say. The same package family has plugins for Rspack and Rsbuild; follow the integration page for your bundler rather than adapting the Webpack snippet.
Whether mf-manifest.json is generated by default or only through the manifest option, which the options page lists, was not verified here. The documentation's examples point remotes at it, so check this in your setup.
What happens at run time
The documentation's manifest page describes the sequence. The producer generates a manifest after its build, describing the modules it exposes, where the remote entry lives, the assets those modules need, the shared dependencies it offers and where its type files are. The host's runtime fetches and parses it, builds a snapshot (a condensed, pre-resolved form of the same information), and resolves the final remoteEntry URL from that. It can also preload the JavaScript and CSS a module needs before loading it.
The remoteEntry.js is the script that provides access to the exposed modules. The error-code page says the loaded container must have an init export (RUNTIME-002), and ties a failing init to shared-configuration mismatches (RUNTIME-015).
Figure 2. From the host's request to a rendered component
How to read this: nothing about the remote's code is known when the host is built, only its name and where to ask. Each arrow is a place where a network request, a version check or an initialisation can fail.
The runtime API
The documentation's runtime API page lists five functions, imported from @module-federation/enhanced/runtime. With the build plugin they act on the default instance.
| Function | Purpose | Shape shown on the page |
|---|---|---|
init |
Initialise or reuse a runtime instance and register remotes | init({ name: 'mf_host', remotes: [{ name: 'remote', entry: 'http://localhost:2001/mf-manifest.json' }] }) |
loadRemote |
Load a remote module at run time by name or alias plus the expose path | loadRemote('remote/util').then((m) => m.add(1, 2, 3)) |
registerRemotes |
Add remotes at run time, with { force: true } to overwrite one |
registerRemotes([{ name: 'sub2', entry: '...' }]) |
loadShare |
Get a shared dependency, reusing a compatible one already present | loadShare('react') |
preloadRemote |
Preload a remote's entry, exposes and resources ahead of time to avoid request waterfalls | preloadRemote([{ nameOrAlias: 'sub2', resourceCategory: 'all' }]) |
The page recommends migrating from init to getInstance() plus the register* functions, or to createInstance(), so check it for the current recommended form.
Loading a remote component in React
For React hosts, the documentation describes a separate package, @module-federation/bridge-react, which it says covers React 16.x through 19.x and handles the differences between those versions. Its createLazyComponent builds in React.lazy, Suspense and error boundaries, so you give it a loader, a loading node and a fallback:
const LazyComponent = instance.createLazyComponent({
loader: () => loadRemote('dynamic_remote'),
loading: 'loading...',
fallback: ({ error }) => <div>fallback</div>,
});
It is enabled once at run time with instance.registerPlugins([lazyLoadComponentPlugin()]). The page also describes prefetch, which starts fetching before the component renders to avoid a waterfall. The snippet is the documentation's own, not run here. In a real project, loadRemote('dynamic_remote') would be your remote's alias and expose path, such as checkout/Panel for a remote registered as checkout that exposes ./Panel, following the documentation's pattern of loadRemote('remote/util').
Sharing React: why singletons matter
React's own documentation explains the problem: if the react imports used by different parts of an app resolve to two different exports objects, hooks fail, and "accidentally end up with two copies of the react package" is one of the three causes it lists. A host and a remote that each bundle their own React are exactly that case.
The shared option is how Module Federation avoids it. The documentation's table of options:
| Option | Meaning | Default |
|---|---|---|
singleton |
Allow only one version of the module in the share scope. The higher version loads, and the lower-version side gets a warning. | false |
requiredVersion |
The minimum acceptable version. A lower version triggers a console warning and uses the smallest available shared version. | The version in the app's package.json |
eager |
Load the shared module immediately by packaging it into the entry file. The page warns this can make the entry file large. | false |
import |
Import path; false means it is not bundled and the consumer must supply it. |
undefined |
shareScope |
Name of the share scope. | 'default' |
The page also says that if one party sets requiredVersion and the other sets singleton, the dependency with requiredVersion loads and the singleton party uses it regardless of version.
Figure 3. What a singleton does when two versions meet
Remote offers another"] --> Q{"singleton: true
on both?"} Q -->|"yes"| H["One version stays in the share scope:
the higher one loads"] H --> W["The side that offered the
lower version gets a warning"] Q -->|"no"| D["Each side can load its own copy:
two copies of React"] D --> X["Hooks can fail:
React names two copies as a cause"]
How to read this: singleton: true trades a separate version per application for one copy everywhere. The warning is the cue to decide which team upgrades.
One documented detail matters for React 18 and 19 apps. The page says request defaults to the shared entry's property name, that it is the module request the bundler intercepts at build time, and that a request without a trailing / (for example react-dom) only intercepts the exact import, so subpath imports such as react-dom/client then load a duplicate copy. Modern React apps start with react-dom/client, so read the shared page's request section and check in the built output that one copy of each is loaded. This was not verified by a build here.
The shareStrategy option controls when shared dependencies are negotiated. The page lists two values: 'version-first', the default set by the bundler runtime, which loads all remote entries at startup to register their shared dependencies and ensures the highest version is used, and 'loaded-first', which reuses shared dependencies already loaded and loads remotes on demand.
Failure handling: three layers
A remote is a network dependency of the host, and each of its loading steps can fail. The documentation's error-handling article describes three layers that can be used separately or together.
Figure 4. Three layers between a failing remote and a broken page
How to read this: each layer handles a different kind of failure. The retry plugin handles transient fetch errors, the runtime hook can return a fallback component or a backup manifest, and a React error boundary catches exceptions thrown while the remote component renders.
- Retry (network layer). The
@module-federation/retry-pluginpackage retries failed fetch and script requests, with a configurable number of attempts and delay, for exampleRetryPlugin({ retryTimes: 3, retryDelay: 1000 }). - The
errorLoadRemotehook (loading layer). A runtime plugin can return a fallback when loading fails. The article distinguishes theonLoadlifecycle (errors loading a module other than the entry manifest, where the plugin returns a fallback component) andafterResolve(errors loading the entrymf-manifest.json, where the plugin can fetch a backup manifest or return a predefined local one). - An error boundary (rendering layer). React's error boundary catches exceptions thrown while a remote component renders and shows a message, for example
<ErrorBoundary fallback={<div>Error loading Remote1App...</div>}>.
The troubleshooting pages give codes to search for. A few that map to the steps in Figure 2:
| Code | Meaning on the documentation page |
|---|---|
| RUNTIME-003 | Failed to get manifest: the URL cannot be reached, or there is a CORS problem |
| RUNTIME-001 | Failed to get remoteEntry exports: the container was not registered, often a wrong URL or network problem |
| RUNTIME-008 | Failed to load script resources: a network failure or an exception while the script ran; retrying does not fix the second kind |
| RUNTIME-004 | Failed to locate remote: no matching remote is registered, or the name or alias is wrong |
| RUNTIME-014 | The remote does not expose the requested module: check the name, the ./ prefix, letter case and whether the deployed build is current |
| RUNTIME-015 | Remote container initialisation failed: the container loaded but init threw, usually because of shared-config mismatches |
Trade-offs
| Dimension | Module Federation | Iframe | Web component with its own bundle |
|---|---|---|---|
| Join | Run time, through the bundler runtime | Run time, separate document | Run time, custom element |
| Sharing dependencies | Built in through shared |
None: each frame loads its own | Not part of the standard; you arrange it |
| Isolation | Shared page and scope by design | Strongest | Shadow DOM isolates styles |
| Coupling | Bundler and its plugin | Almost none | Browser standards |
| Main risk | Shared version negotiation and remote availability | Routing, history, sizing | Duplicate dependencies |
In practice: the storefront shell loads a Checkout remote
This is a realistic scenario, not a case history. It is a composite built from common production patterns, describes no specific company or product, and every number is an illustrative assumption.
The situation
The storefront shell is a React application. The Checkout team wants to release payments changes without waiting for the shell's release. Both teams use React. The shell team owns navigation, the header and the sign-in state. The decision to split Checkout out has already been made on organisational grounds, as in the first lesson of this module. The open question is how to load it.
Starting architecture
Figure 5. Checkout compiled into the shell
How to read this: Checkout's change is a change to the shell's build, so it ships on the shell's schedule.
Options considered
| Option | What it fixes | What it costs | Risk |
|---|---|---|---|
| Keep Checkout as a package | Nothing new | Nothing | Lockstep releases remain |
| Iframe for Checkout | Strong isolation | Routing, history and sizing work | Awkward user experience |
| Module Federation remote for Checkout | Independent build and release, shared React | Bundler plugin, manifest hosting, version policy | Host depends on the remote at start-up |
| Web component with its own bundle | Browser-standard boundary | Second copy of React unless arranged | Heavier first load |
The decision
| Field | Decision record |
|---|---|
| Context | Both apps are React and use the same bundler. Checkout needs its own release cadence. |
| Decision | Checkout becomes a federated remote that exposes one panel. The shell registers it and loads it with a loading state and an error fallback. React and React DOM are singleton on both sides. |
| Consequences, good | Checkout releases without a shell release. One copy of React is loaded. |
| Consequences, bad | The shell now depends on a remote address and a manifest. Both teams must agree on how React is upgraded. |
| Revisit when | A second team needs a different React version, or the shared list grows beyond a few dependencies. Then reconsider the contract or the isolation level. |
Rollout and verification
This is the plan a team would follow; none of it was run for this lesson.
- Build the remote and the shell in a throwaway repository first, with the minimal configuration above, and confirm in the browser's network panel that the manifest and the remote entry load.
- Check that exactly one copy of React and React DOM is loaded, including the
react-dom/clientimport. Open the console and read any shared-version warnings. - Add the three failure layers and test each by blocking the manifest, blocking the entry script and throwing from the panel.
- Release behind a flag so the shell can fall back to the in-repository Checkout while the remote is proven.
- Watch field load metrics and the error rate for the remote's requests after rollout.
The 2 a.m. failure
At night the CDN path that serves the Checkout remote becomes unreachable. The shell, left on the default version-first strategy, loads every remote's entry at start-up to register shared dependencies. Pages that used to open in a normal time hang or fail to initialise, including pages that never use Checkout.
The documentation warns of this case: with version-first, an unreachable remote at start-up triggers the errorLoadRemote hook with the beforeLoadShare lifecycle, and without handling, initialisation may hang or fail. With loaded-first, failures appear only when that specific remote is accessed.
First three checks: (1) look for RUNTIME-003 or RUNTIME-001 errors in the shell's client logs; (2) check which shareStrategy the shell uses; (3) check whether an errorLoadRemote handler covers beforeLoadShare. Mitigation: switch the shell to the flagged in-repository Checkout, or restore the remote's path. Follow-up: add an errorLoadRemote handler for the start-up case, evaluate loaded-first for remotes that are not needed on every page, and add a test that blocks the remote at start-up.
Architect's notes
- Decide who owns the shared list. Every shared dependency is a promise between teams. Keep the list short and name an owner.
- Treat the remote's address and manifest as production dependencies. They need availability targets, caching rules and a rollback path.
- Test the host with the remote down. The start-up path is the most surprising.
- Check one copy of React in the built output, not only in configuration.
- Defer clever features. Start with
exposes,remotesand singletons for React; add runtime plugins and preloading when measurements ask for them. - Read the version of the documentation that matches your plugin. The project distinguishes 2.0 from the Webpack 5 built-in feature.
Common mistakes
- Sharing nothing. Every remote then ships its own React and hooks can fail.
- Forgetting subpath imports.
react-dom/clientmay not be covered by a shared entry forreact-dom, according to the shared page. - Making everything eager. The page warns
eagerpacks the module into the entry file. - Loading all remotes at start-up and assuming they are up. See the 2 a.m. failure.
- No error boundary around a remote component. A throw during render takes the host subtree down.
- Pointing
remotesat a changing address with no rollback plan.
Key takeaways
- Module Federation lets a host load modules that a remote builds and deploys on its own, joined at run time through the bundler.
- A producer sets
nameandexposes; a consumer setsremotesin the formname@entry; both setshared. - The entry is either
mf-manifest.jsonorremoteEntry.js; the manifest adds type hints, preloading and devtools support. - Share React and React DOM as singletons: two copies are a documented cause of hook failures. Check subpath imports.
shareStrategydecides whether every remote is loaded at start-up (version-first, the default) or on demand (loaded-first).- The runtime API (
init,loadRemote,registerRemotes,loadShare,preloadRemote) lets code register and load remotes without static configuration. - Protect the page with three layers: retry, the
errorLoadRemotehook and an error boundary. - Nothing here was run: verify configuration against the documentation and in your own build.
Practice exercises
Exercise 1 — Starter (30 min): From the options table, write the producer and consumer configuration for a remote named catalogue that exposes a ./ProductGrid component, sharing React and React DOM. Check each option against the configuration pages.
Exercise 2 — Intermediate (2 hours): In a throwaway project with two applications, set up the minimal host and remote from the documentation's quick start. Confirm in the network panel which requests load, and record the order of the manifest, entry and module requests.
Exercise 3 — Advanced (half day): Add the three failure layers to the host from Exercise 2. Block the manifest, then the entry script, then make the remote component throw, and record what the user sees in each case and the matching error code.
Related reading
In this course:
- Monolith vs Micro-Frontends, whether to split at all.
- Micro-Frontend Composition, the other places the join can happen.
- Code Splitting, Lazy Loading and Tree Shaking, the per-chunk loading that federation builds on.
- React Reconciliation and the Virtual DOM, why a second copy of React matters.
Official documentation
- Module Federation: Introduction and Glossary of terms.
- Module Federation: Quick start and the Webpack integration.
- Configuration: options overview, exposes, remotes, shared and shareStrategy.
- Module Federation: Runtime API and Manifest and Snapshot.
- Module Federation: Error handling for remote modules and runtime error codes.
- React: Invalid hook call warning.