Effortless Dynamic Module Loading with Module Federation V2 in React

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‑inModuleFederationPlugin
.Runtime API: methods like
init()
andloadRemote()
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
Start the remote on port 4000:
cd remote npm run start # serves remoteEntry.js and HTML
Start the host on port 3000:
cd host npm run start
Open http://localhost:3000. You should see the Host heading and the remote widget below it.
8. Pros & Cons vs. Standard Webpack MF
Aspect | MF 1.x (Webpack) | MF V2 (Enhanced) |
Runtime API | Manual __webpack_init_sharing__ + container.init | Clean init() + loadRemote() |
Share Scope Management | Each host has its own, can collide | Single global share scope, idempotent inits |
Dynamic URLs | Must bake remotes at build time | Can register or override remotes at runtime easily |
Multiple Hosts | Risk of "already initialized" errors | No conflicts: caches and reuses remotes |
Bundle Size | Larger initial host if eager share used | Minimal, can load remotes only when needed |
Flexibility | Statically defined remotes in webpack.config.js | Dynamic registration, easier multi-env overrides |
Learning Curve | Lower (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! 🚀
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.