Skip to content
Codeloom
JavaScript

JavaScript Module Federation Explained

Understand Module Federation for micro-frontends -- share dependencies at runtime, load remote modules dynamically, and compose independent apps into one.

·6 min read · By Codeloom
Advanced 12 min read

What you'll learn

  • What Module Federation is and the problems it solves
  • How to configure hosts and remotes with Webpack
  • Strategies for sharing dependencies and handling versioning

Prerequisites

  • JavaScript ES Modules
  • Basic Webpack configuration
  • Understanding of micro-frontend architecture

Module Federation is a Webpack 5 feature that lets multiple independently built and deployed JavaScript applications share code at runtime. Instead of bundling everything into a single monolithic build, each application exposes modules that other applications can consume — without npm packages, monorepos, or iframe hacks.

The problem Module Federation solves

In a traditional web application, all code is bundled together at build time. As the application grows, this creates problems:

  • Long build times — every change rebuilds the entire app.
  • Tight coupling — teams cannot deploy independently.
  • Duplicate dependencies — multiple apps include their own copy of React, lodash, etc.

Micro-frontend architectures address these problems by splitting the frontend into independent applications. But they introduce a new challenge: how do you share code and dependencies between independently deployed apps without duplicating them?

Module Federation solves this by making dependency sharing a runtime concern rather than a build-time one.

Core concepts

Host and Remote

  • Remote — an application that exposes modules for others to consume.
  • Host — an application that consumes modules from remotes.

An application can be both a host and a remote simultaneously.

Shared dependencies

Module Federation lets applications declare which dependencies they want to share. At runtime, if two applications both need React 18, only one copy is loaded. If they need different major versions, both are loaded to maintain compatibility.

Setting up a remote

A remote application exposes specific modules through its Webpack configuration.

// webpack.config.js (remote: "catalog")
const { ModuleFederationPlugin } = require("webpack").container;

module.exports = {
  output: {
    publicPath: "https://catalog.example.com/",
    uniqueName: "catalog",
  },
  plugins: [
    new ModuleFederationPlugin({
      name: "catalog",
      filename: "remoteEntry.js",
      exposes: {
        "./ProductList": "./src/components/ProductList",
        "./ProductDetail": "./src/components/ProductDetail",
        "./utils/pricing": "./src/utils/pricing",
      },
      shared: {
        react: { singleton: true, requiredVersion: "^18.0.0" },
        "react-dom": { singleton: true, requiredVersion: "^18.0.0" },
      },
    }),
  ],
};

The exposes field maps public module names to internal file paths. The filename specifies the entry point that hosts will load.

Setting up a host

The host application declares which remotes it wants to consume.

// webpack.config.js (host: "shell")
const { ModuleFederationPlugin } = require("webpack").container;

module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: "shell",
      remotes: {
        catalog: "catalog@https://catalog.example.com/remoteEntry.js",
      },
      shared: {
        react: { singleton: true, requiredVersion: "^18.0.0" },
        "react-dom": { singleton: true, requiredVersion: "^18.0.0" },
      },
    }),
  ],
};

Now the host can import modules from the remote as if they were local:

// In the host application
import ProductList from "catalog/ProductList";
import { calculateDiscount } from "catalog/utils/pricing";

function App() {
  return (
    <div>
      <h1>Our Store</h1>
      <ProductList onPriceCalc={calculateDiscount} />
    </div>
  );
}

Dynamic remotes

Hardcoding remote URLs in the Webpack config is limiting. Dynamic remotes let you load modules from URLs determined at runtime.

async function loadRemoteModule(remoteUrl, scope, module) {
  // Load the remote entry script
  await new Promise((resolve, reject) => {
    const script = document.createElement("script");
    script.src = remoteUrl;
    script.onload = resolve;
    script.onerror = reject;
    document.head.appendChild(script);
  });

  // Initialize the remote container
  const container = window[scope];
  await container.init(__webpack_share_scopes__.default);

  // Get the module factory
  const factory = await container.get(module);
  return factory();
}

// Usage
const ProductList = await loadRemoteModule(
  "https://catalog.example.com/remoteEntry.js",
  "catalog",
  "./ProductList"
);

This approach is powerful for A/B testing, feature flags, or loading different versions of a module based on user context.

Shared dependency strategies

The shared configuration controls how dependencies are deduplicated across applications.

Singleton

Forces a single instance of a library. Critical for libraries like React that break when multiple instances are loaded.

shared: {
  react: {
    singleton: true,
    requiredVersion: "^18.0.0",
    strictVersion: true, // Throw an error if versions are incompatible
  },
}

Eager loading

By default, shared modules are loaded lazily. Set eager: true to include them in the initial bundle.

shared: {
  lodash: {
    eager: true, // Include in the initial bundle, don't lazy-load
  },
}

Version ranges

shared: {
  axios: {
    requiredVersion: "^1.4.0",
    // If the host provides axios 1.5.0, use that
    // If the host provides axios 0.27.0, load our own copy
  },
}

Error handling and fallbacks

Remote modules can fail to load — the server might be down, or a network error might occur. Always handle this gracefully.

// React example with error boundary and lazy loading
import React, { Suspense, lazy } from "react";

const RemoteProductList = lazy(() =>
  import("catalog/ProductList").catch(() => {
    // Fallback to a local component if the remote fails
    return import("./FallbackProductList");
  })
);

function App() {
  return (
    <ErrorBoundary fallback={<div>Something went wrong.</div>}>
      <Suspense fallback={<div>Loading products...</div>}>
        <RemoteProductList />
      </Suspense>
    </ErrorBoundary>
  );
}

Framework-agnostic usage

Module Federation is not limited to React. Any JavaScript module can be federated.

// Remote: exposes a vanilla JS utility
// webpack.config.js
exposes: {
  "./analytics": "./src/analytics.js",
}

// analytics.js
export function trackEvent(name, data) {
  fetch("/api/events", {
    method: "POST",
    body: JSON.stringify({ name, data, timestamp: Date.now() }),
  });
}

// Host: consumes it in any framework
import { trackEvent } from "analytics/analytics";
trackEvent("page_view", { page: "/home" });

Beyond Webpack: Module Federation 2.0

The original Module Federation was tightly coupled to Webpack. Module Federation 2.0 (via the @module-federation/enhanced package) adds:

  • Framework support — official plugins for Rspack, Vite, and others.
  • Type safety — automatic TypeScript type generation for remote modules.
  • Runtime API — programmatic control over loading and versioning.
// vite.config.js with Module Federation 2.0
import { federation } from "@module-federation/vite";

export default {
  plugins: [
    federation({
      name: "host",
      remotes: {
        catalog: {
          type: "module",
          name: "catalog",
          entry: "https://catalog.example.com/remoteEntry.js",
        },
      },
      shared: ["react", "react-dom"],
    }),
  ],
};

When to use Module Federation

Module Federation is a good fit when:

  • Multiple teams work on different parts of the frontend and need independent deployment.
  • Shared UI libraries need to be updated without rebuilding all consumers.
  • Large applications have long build times that can be reduced by splitting into smaller units.
  • A/B testing requires loading different component versions at runtime.

It is likely overkill for small projects with a single team or when a simple npm package would suffice.

Summary

Module Federation enables runtime code sharing between independently deployed JavaScript applications:

  • Remotes expose modules; hosts consume them via standard import syntax.
  • Shared dependencies are deduplicated at runtime, avoiding duplicate copies of React or other libraries.
  • Dynamic remotes let you load modules from URLs determined at runtime.
  • Error boundaries and fallbacks are essential for production resilience.
  • Module Federation 2.0 extends support beyond Webpack to Vite, Rspack, and other bundlers.

It is the most practical solution for micro-frontend architectures that need to share code without the complexity of iframes or the coupling of monorepos.