Skip to content

    Frontend System Design

    Module Federation in React: Hosts, Remotes and Shared Dependencies

    Read and reason about Module Federation configuration for a React app: exposes, remotes, shared singletons, the runtime API and how failures are handled.

    By Published 16 min read

    • frontend-system-design
    • module-federation
    • micro-frontends
    • react
    Share

    Flagship lessonadvancedReact labFrontend System Design

    Before this: Micro-Frontend Composition: Build-Time, Runtime, Iframes and the Edge

    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, remotes and shared, 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 exposes setting.
    • Consume. A consumer (also called a host) lists the producers it loads from, with the remotes setting. 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 shared setting.

    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

    A host loads an exposed module from a remote while both share ReactShopper to Host (opens page); Host to Manifest (reads); Host to Remote: checkout (loads Panel); Host to Shared React (uses); Remote: checkout to Shared React (uses)TEAM B DEPLOYSShopperShopperHost: storefront shellHoststorefront shellManifest: exposes and sharesManifestexposes andsharesRemote: checkout: exposes: ./PanelRemote:checkoutexposes: ./PanelShared React: one copy, singletonShared Reactone copy,singletonopens pagereadsloads Panelusesuses

    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

    sequenceDiagram accTitle: How a host loads a federated module accDescr: The host runtime reads the remote manifest, builds a snapshot, loads the remote entry, initialises shared dependencies, and gets the exposed module, which the host renders. participant H as Host runtime participant M as Manifest participant R as Remote entry H->>M: fetch mf-manifest.json M-->>H: exposes, assets, shared, entry URL H->>H: build snapshot, resolve entry H->>R: load remote entry script H->>R: init with the shared scope H->>R: get exposed module R-->>H: the module H->>H: render the 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

    flowchart TD accTitle: Choosing among shared versions of React accDescr: Two applications offer React. With singleton set, only one version is kept in the share scope, the higher one loads, and the application that offered the lower version gets a warning. Without singleton, each side can load its own copy. A["Host offers one version of react
    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

    Retry, load fallback and render fallback protect the host from a failing remoteRemote request to Retry plugin (fails); Retry plugin to Load hook (still fails); Load hook to Error boundary (render throws); Error boundary to Page keeps workingRemote request: manifest, entryRemote requestmanifest, entryRetry plugin: network layerRetry pluginnetwork layerLoad hook: errorLoadRemoteLoad hookerrorLoadRemoteError boundary: rendering layerError boundaryrendering layerPage keeps working: fallback shownPage keepsworkingfallback shownfailsstill failsrender throws

    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.

    1. Retry (network layer). The @module-federation/retry-plugin package retries failed fetch and script requests, with a configurable number of attempts and delay, for example RetryPlugin({ retryTimes: 3, retryDelay: 1000 }).
    2. The errorLoadRemote hook (loading layer). A runtime plugin can return a fallback when loading fails. The article distinguishes the onLoad lifecycle (errors loading a module other than the entry manifest, where the plugin returns a fallback component) and afterResolve (errors loading the entry mf-manifest.json, where the plugin can fetch a backup manifest or return a predefined local one).
    3. 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

    NOTE

    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

    Starting architecture: Checkout is a folder inside the shell's one buildShell team to One build; Checkout team to One build; One build to One deployed app (one release)Shell team: navigation, headerShell teamnavigation,headerCheckout team: payments panelCheckout teampayments panelOne build: one React, one bundleOne buildone React, onebundleOne deployed app: shell + CheckoutOne deployedappshell + Checkoutone release

    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.

    1. 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.
    2. Check that exactly one copy of React and React DOM is loaded, including the react-dom/client import. Open the console and read any shared-version warnings.
    3. Add the three failure layers and test each by blocking the manifest, blocking the entry script and throwing from the panel.
    4. Release behind a flag so the shell can fall back to the in-repository Checkout while the remote is proven.
    5. 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, remotes and 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/client may not be covered by a shared entry for react-dom, according to the shared page.
    • Making everything eager. The page warns eager packs 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 remotes at 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 name and exposes; a consumer sets remotes in the form name@entry; both set shared.
    • The entry is either mf-manifest.json or remoteEntry.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.
    • shareStrategy decides 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 errorLoadRemote hook 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.

    In this course:

    Official documentation

    Ask about this article

    Get answers grounded in this post. AI-generated — based on this article, and may be imperfect.

    Scaled AI Weekly

    Enjoyed this? Get more like it every Monday.

    Real architecture decisions, LLMOps patterns that survive production, and engineering leadership advice — from 12+ years of building at enterprise scale. Free. No spam. Unsubscribe anytime.

    Get new articles by email

    Free. We email a confirmation link first, and you can unsubscribe from any email.

    Join engineers building production AI systems

    Comments