When Webpack 5 was officially released in October, the frontend community couldn’t stop talking about one feature in particular: Module Federation. After years of grappling with increasingly complex frontend architectures, the promise of being able to seamlessly share code between applications at runtime—not just build time—feels like the solution many of us have been waiting for.

If you follow frontend architecture discussions, you’ve likely seen the buzz around Module Federation since earlier this year when Zack Jackson first introduced the concept. As he aptly put it, this is “the JavaScript bundler equivalent of what Apollo did with GraphQL” - a truly revolutionary approach to code sharing.

After watching presentation videos and reading articles about the theoretical benefits, I finally had time to roll up my sleeves and try it myself. In this post, I’ll walk through my first experiment with Module Federation in a small-scale project, showing you how to set it up and highlighting the interesting discoveries I made along the way.

Understanding the Problem

Before diving into the code, let’s briefly outline the problem Module Federation is trying to solve. Traditionally, we’ve had a few approaches to share code between applications:

  1. NPM packages: Extract shared code into libraries, publish them, and import them into each application. This works but creates a tight coupling at build time and requires a full rebuild and deployment cycle for updates.

  2. Monorepos: Keep all applications in a single repository with shared components. This helps with consistent versioning but doesn’t solve the runtime dependency problem.

  3. iframes or runtime loading: These approaches often introduce their own complexities and limitations.

Module Federation offers a new approach: applications can expose and consume parts of their codebase at runtime without having to package and deploy them separately. As InfoQ quoted Zack Jackson, the motivation was clear: “Sharing code is not easy. Depending on your scale, it can even be unprofitable.”

Setting Up Our Experiment

For my experiment, I created two simple applications:

  1. Host App: The main application that will consume components from the remote app.
  2. Remote App: A secondary application that exposes components to be consumed by the host.

Both applications are basic React apps, but the principles apply to any JavaScript framework. Let’s start by setting up the configuration for each.

The Remote App Configuration

First, we need to configure our remote app to expose certain modules. Here’s what my webpack configuration looks like:

const { ModuleFederationPlugin } = require("webpack").container;
const path = require("path");
const deps = require("./package.json").dependencies;

module.exports = {
  entry: "./src/index",
  mode: "development",
  devServer: {
    contentBase: path.join(__dirname, "dist"),
    port: 8081,
  },
  output: {
    publicPath: "http://localhost:8081/",
  },
  module: {
    rules: [
      {
        test: /\.jsx?$/,
        loader: "babel-loader",
        exclude: /node_modules/,
        options: {
          presets: ["@babel/preset-react"],
        },
      },
    ],
  },
  plugins: [
    new ModuleFederationPlugin({
      name: "remote_app",
      filename: "remoteEntry.js",
      exposes: {
        "./Button": "./src/components/Button",
        "./Header": "./src/components/Header",
      },
      shared: {
        react: {
          singleton: true,
          requiredVersion: deps.react,
        },
        "react-dom": {
          singleton: true,
          requiredVersion: deps["react-dom"],
        },
      },
    }),
  ],
};

The key part here is the ModuleFederationPlugin configuration:

  • name: The name of our remote app, which will be used by the host to reference it.
  • filename: The name of the entry file that will be generated.
  • exposes: The modules we want to expose to other applications. In this case, I’m exposing two React components.
  • shared: Dependencies that should be shared between the host and remote. This is critical to avoid loading multiple instances of React.

The Host App Configuration

Now, let’s configure our host app to consume the components exposed by the remote:

const { ModuleFederationPlugin } = require("webpack").container;
const path = require("path");
const deps = require("./package.json").dependencies;

module.exports = {
  entry: "./src/index",
  mode: "development",
  devServer: {
    contentBase: path.join(__dirname, "dist"),
    port: 8080,
  },
  output: {
    publicPath: "http://localhost:8080/",
  },
  module: {
    rules: [
      {
        test: /\.jsx?$/,
        loader: "babel-loader",
        exclude: /node_modules/,
        options: {
          presets: ["@babel/preset-react"],
        },
      },
    ],
  },
  plugins: [
    new ModuleFederationPlugin({
      name: "host_app",
      remotes: {
        remote_app: "remote_app@http://localhost:8081/remoteEntry.js",
      },
      shared: {
        react: {
          singleton: true,
          requiredVersion: deps.react,
        },
        "react-dom": {
          singleton: true,
          requiredVersion: deps["react-dom"],
        },
      },
    }),
  ],
};

The key differences in the host configuration:

  • Instead of exposes, we have remotes which defines the remote applications we want to consume from.
  • The format is "name": "remoteName@remoteUrl/filename", where the name, remoteName, and filename match what we defined in the remote app.

Using Remote Components in the Host App

With the configuration in place, we can now use the remote components in our host app. However, there’s an important detail: since the remote modules are loaded at runtime, we need to use dynamic imports.

Here’s how I structured my host app’s entry point:

// src/bootstrap.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

ReactDOM.render(<App />, document.getElementById('root'));

// src/index.js
import('./bootstrap');

And here’s how I used the remote components in my App.js:

import React, { Suspense, lazy } from 'react';

// Import remote components using dynamic import
const RemoteButton = lazy(() => import('remote_app/Button'));
const RemoteHeader = lazy(() => import('remote_app/Header'));

const App = () => {
  return (
    <div>
      <h1>Host Application</h1>
      <Suspense fallback={<div>Loading Header...</div>}>
        <RemoteHeader />
      </Suspense>
      <div>
        <p>This is some local content in the host app.</p>
        <Suspense fallback={<div>Loading Button...</div>}>
          <RemoteButton />
        </Suspense>
      </div>
    </div>
  );
};

export default App;

Notice how I’m using React’s Suspense and lazy to handle the asynchronous loading of the remote components. This is a key pattern when working with Module Federation.

What I Learned: Key Insights

After getting this basic example working, here are some interesting things I discovered:

1. The Runtime Connection

One of the most fascinating aspects is that the host application is loading the special remoteEntry.js file at runtime. This JavaScript file is only a few KB in size and serves as the orchestration layer between the applications. It’s not a traditional entry point but rather a specialized Webpack runtime that provisions the connection to other Webpack builds.

2. Shared Dependencies Are Crucial

The shared configuration is more important than I initially realized. If React isn’t properly shared between applications, you can end up with multiple instances of React running, which leads to “Invalid Hook Calls” and other strange behaviors. By setting singleton: true, we ensure only one instance of React is loaded.

3. Version Compatibility Is Handled

Module Federation can intelligently handle version compatibility between shared dependencies. If the host and remote specify compatible versions (using semver), everything works smoothly. If there’s a conflict, Webpack will warn you and may load multiple versions depending on your configuration.

4. Development Experience

During development, I found it quite powerful that I could make changes to the remote app, and upon refresh, those changes would be immediately reflected in the host application. This creates a development experience that feels surprisingly cohesive despite working with separate builds.

Practical Applications

While my experiment was small-scale, it’s easy to see how this technology could be applied to larger systems:

  1. Micro-frontends: Teams can develop and deploy their parts of an application independently, with a main shell application that loads them at runtime.

  2. Design Systems: A central team could maintain a design system that other applications consume at runtime, ensuring everyone always has the latest components without needing to update packages.

  3. Feature Flagging: You could dynamically load different implementations of features based on user preferences or A/B testing requirements.

  4. Gradual Migrations: When migrating from one framework to another, you could have parts of your application in the old framework and parts in the new one, all working together seamlessly.

Limitations and Considerations

While my experience with Module Federation has been mostly positive, there are some considerations to keep in mind:

  1. Network Dependencies: Your application now has runtime dependencies on other applications, which introduces network-related failure modes.

  2. Versioning Strategy: You need a clear strategy for versioning exposed modules to avoid breaking changes.

  3. Debugging Complexity: Debugging across module boundaries can be more complex than with a monolithic application.

  4. Browser Support: While the webpack runtime works in all modern browsers, you’ll need to ensure your transpilation settings accommodate your target browsers.

Conclusion

Module Federation represents a significant evolution in how we can structure JavaScript applications, and the excitement around it is well-justified. My small experiment has convinced me that this approach has real potential for improving how we build complex frontend systems. As Paweł Szonecki recently noted, “the effort put in at the beginning of its implementation will pay off in the long run during the project.”

The Webpack team and community contributors have provided us with what might be the most significant advancement in JavaScript architecture this year. If you’re dealing with complex frontend applications or considering a micro-frontend approach, I highly recommend giving Module Federation a try. The setup is surprisingly straightforward once you understand the core concepts, and the benefits for code sharing and independent deployments are substantial.

The code examples are simplified versions of what I used in my actual experiment, but they capture the essence of how to get started.