Effortless Dynamic Module Loading with Module Federation V2 in React

Adeesh SharmaAdeesh Sharma
5 min read

Module Federation V2 (MFv2) brings new runtime APIs and improved share‑scope handling to the micro‑frontend ecosystem. In this post, we’ll build a minimal React host and remote using the @module-federation/enhanced runtime, show full example code, and compare the pros and cons against the classic Webpack Module Federation (MF 1.x).


1. Introduction

Micro‑frontends let teams ship independently deployable chunks of UI. Webpack Module Federation (since v5) enabled this by letting one app (the host) dynamically load code from another (the remote). However, in MF 1.x:

  • Share scopes could collide when multiple hosts loaded the same remote.

  • Dynamic remotes required manual __webpack_init_sharing__ calls.

  • Remote URLs were baked in at build time (hard to swap at runtime).

MFv2’s enhanced runtime solves these pain points with:

  • A standalone runtime package (@module-federation/enhanced/runtime).

  • A global share scope and idempotent remote initialization.

  • Clean init() + loadRemote() APIs for dynamic loading.


2. What Is Module Federation V2?

  • Build plugin: @module-federation/enhanced/webpack, a drop‑in replacement for Webpack’s built‑in ModuleFederationPlugin.

  • Runtime API: methods like init() and loadRemote() decouple runtime logic from Webpack’s bootstrap.

  • Global share scopes: ensures a single shared dependency map across all hosts/remotes on the page.

This separation means you can register remotes once, swap URLs at runtime, and avoid duplicate container.init errors.


3. Prerequisites

  • Node.js ≥ 16, npm or Yarn.

  • React 18+ (or compatible).

  • Webpack 5.

  • Basic familiarity with Module Federation concepts.

Install dependencies in both projects:

npm install react react-dom
npm install --save-dev webpack webpack-cli webpack-dev-server babel-loader @babel/preset-react html-webpack-plugin
npm install --save @module-federation/enhanced

4. Remote App Setup

4.1 webpack.config.js

// remote/webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { ModuleFederationPlugin } = require('@module-federation/enhanced/webpack');

module.exports = {
  mode: 'development',
  entry: './src/index.jsx',
  output: {
    publicPath: 'auto',
    uniqueName: 'remote_app',
  },
  resolve: { extensions: ['.jsx', '.js'] },
  module: { rules: [ { test: /\.jsx?$/, loader: 'babel-loader', exclude: /node_modules/ } ] },
  plugins: [
    new HtmlWebpackPlugin({ template: './public/index.html' }),
    new ModuleFederationPlugin({
      name: 'remote_app',
      filename: 'remoteEntry.js',
      exposes: {
        './Widget': './src/Widget.jsx'
      },
      shared: {
        react: { singleton: true, eager: true, requiredVersion: '^18.0.0' },
        'react-dom': { singleton: true, eager: true, requiredVersion: '^18.0.0' }
      },
    }),
  ],
};

4.2 Remote Component

// remote/src/Widget.jsx
import React, { useContext } from 'react';

export default function Widget({ message }) {
  return <div style={{ padding: 20, background: '#eef' }}>Remote says: {message}</div>;
}

5. Host App Setup

5.1 webpack.config.js

// host/webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { ModuleFederationPlugin } = require('@module-federation/enhanced/webpack');

module.exports = {
  mode: 'development',
  entry: './src/index.jsx',
  output: {
    publicPath: 'auto',
    clean: true,
  },
  resolve: { extensions: ['.jsx', '.js'] },
  module: { rules: [ { test: /\.jsx?$/, loader: 'babel-loader', exclude: /node_modules/ } ] },
  plugins: [
    new HtmlWebpackPlugin({ template: './public/index.html' }),
    new ModuleFederationPlugin({
      name: 'host_app',
      shared: {
        react: { singleton: true, eager: true, requiredVersion: '^18.0.0' },
        'react-dom': { singleton: true, eager: true, requiredVersion: '^18.0.0' }
      },
    }),
  ],
  devServer: { port: 3000, historyApiFallback: true },
};

6. React Integration with Enhanced Runtime

In your host app’s React entry:

// host/src/App.jsx
import React, { Suspense, lazy, useEffect, useState } from 'react';
import { init, loadRemote } from '@module-federation/enhanced/runtime';

export default function App() {
  const [Widget, setWidget] = useState(null);

  useEffect(() => {
    async function load() {
      // 1) Initialize share scope and register remote URL
      await init({
        name: 'host_app',
        remotes: [ { name: 'remote_app', entry: 'http://localhost:4000/remoteEntry.js' } ]
      });

      // 2) Lazy-load the exposed module
      const LazyWidget = lazy(() =>
        loadRemote('remote_app/Widget').then(mod => ({ default: mod.default }))
      );
      setWidget(() => LazyWidget);
    }

    load();
  }, []);

  return (
    <div>
      <h1>Host</h1>
      {Widget ? (
        <Suspense fallback="Loading remote..."><Widget message="Hello from Host!" /></Suspense>
      ) : (
        <p>Initializing...</p>
      )}
    </div>
  );
}
// host/src/index.jsx
import React from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';

const root = createRoot(document.getElementById('root'));
root.render(<App />);

7. Running the Example

  1. Start the remote on port 4000:

     cd remote
     npm run start  # serves remoteEntry.js and HTML
    
  2. Start the host on port 3000:

     cd host
     npm run start
    
  3. Open http://localhost:3000. You should see the Host heading and the remote widget below it.


8. Pros & Cons vs. Standard Webpack MF

AspectMF 1.x (Webpack)MF V2 (Enhanced)
Runtime APIManual __webpack_init_sharing__ + container.initClean init() + loadRemote()
Share Scope ManagementEach host has its own, can collideSingle global share scope, idempotent inits
Dynamic URLsMust bake remotes at build timeCan register or override remotes at runtime easily
Multiple HostsRisk of "already initialized" errorsNo conflicts: caches and reuses remotes
Bundle SizeLarger initial host if eager share usedMinimal, can load remotes only when needed
FlexibilityStatically defined remotes in webpack.config.jsDynamic registration, easier multi-env overrides
Learning CurveLower (built into Webpack)Slightly higher (install enhanced runtime)

9. Conclusion

Module Federation V2’s enhanced runtime simplifies dynamic micro‑frontend loading in React apps. By centralising share‑scope management and offering straightforward APIs, it avoids the pitfalls of classic MF—especially in multi‑host or multi‑remote scenarios. While it adds a small dependency and learning step, the benefits of runtime flexibility, conflict‑free share scopes, and leaner bundles make it a compelling upgrade path.

Happy micro‑frontending! 🚀

0
Subscribe to my newsletter

Read articles from Adeesh Sharma directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Adeesh Sharma
Adeesh Sharma

Adeesh is an Associate Architect in Software Development and a post graduate from BITS PILANI, with a B.E. in Computer Science from Osmania University. Adeesh is passionate about web and software development and strive to contribute to technical product growth and decentralized communities. Adeesh is a strategic, diligent, and disciplined individual with a strong work ethic and focus on maintainable and scalable software.