Mastering Module Federation: A Beginner’s Guide to Scalable Micro-Frontends (2025 Edition)
1. Introduction to Module Federation
Welcome to the exciting world of Module Federation! This guide is designed for absolute beginners who want to understand and implement this powerful technology to build scalable and maintainable web applications.
What is Module Federation?
Module Federation, introduced in Webpack 5, is a groundbreaking feature that allows multiple, independently built and deployed JavaScript applications to share code and assets at runtime. Instead of bundling all dependencies into a single, monolithic application, Module Federation enables dynamic loading of modules from other applications (known as “remotes”) into a “host” application.
Imagine you have several distinct parts of a large web application—like a product catalog, a shopping cart, and a user profile—each developed by different teams and potentially using different frameworks. Before Module Federation, sharing components or logic between these parts was often a complex dance of publishing libraries to NPM and dealing with version conflicts. Module Federation simplifies this by allowing these independent applications to expose and consume modules directly at runtime, fostering a true micro-frontend architecture.
Why Learn Module Federation? (Benefits, Use Cases, Industry Relevance)
Learning Module Federation is becoming increasingly crucial in modern web development, especially for large-scale projects. Here’s why:
- Enables Micro-Frontend Architecture: This is the primary driver. Module Federation provides a robust solution for breaking down large, monolithic frontends into smaller, manageable, and independently deployable units. This allows for greater team autonomy, faster development cycles, and easier scaling of development efforts.
- Independent Development & Deployment: Teams can develop, build, and deploy their micro-frontends without affecting other parts of the application. This reduces coordination overhead and allows for continuous delivery of features.
- Code Sharing & Reduced Duplication: Instead of duplicating common libraries (like React, Angular, or Vue) across multiple applications, Module Federation allows you to share them. This significantly reduces bundle sizes and improves performance by ensuring a single instance of a shared library is loaded.
- Framework Agnosticism (with caveats): While often used within a single framework ecosystem (e.g., all React apps), Module Federation can facilitate cross-framework integration, allowing you to combine micro-frontends built with different technologies into a unified experience.
- Improved Performance: By dynamically loading only the necessary modules on demand (lazy loading), Module Federation can reduce the initial load time of your application.
- Enhanced Maintainability: Smaller, self-contained micro-frontends are easier to understand, debug, and maintain compared to large, complex monoliths.
- Industry Relevance: Major companies like Spotify and Salesforce are leveraging micro-frontend architectures, often powered by Module Federation, to manage their complex platforms. As web applications grow, the demand for developers proficient in this pattern will only increase.
A Brief History
Before Module Federation, various techniques were used to achieve modularity in frontends, such as iframes, build-time integration (NPM packages), and Web Components. While these offered some benefits, they often came with limitations like poor user experience, centralized release cycles, or complex state management.
Webpack 5 introduced Module Federation in 2020 as a direct answer to these challenges, providing a more robust and native solution for runtime module sharing. It quickly gained traction, and with the release of Module Federation 2.0 in 2024, the ecosystem has matured further, offering enhanced features like:
- Decoupled Runtime: The runtime capabilities are now extracted to a standalone SDK, allowing for greater flexibility and standardization across different build tools (Webpack, Rspack).
- Type Safety: Automatic generation and loading of types for TypeScript projects.
- Debugging Tools: Enhanced debugging with a new Chrome DevTools.
- Deployment Platforms Integration: Simplifies integration with deployment platforms via the
mf-manifest.jsonfile protocol.
Setting Up Your Development Environment
To follow along with this guide, you’ll need a basic development environment. We’ll primarily focus on a JavaScript/React setup using Node.js and a package manager.
Prerequisites:
Node.js: Module Federation relies on Node.js for running build tools and development servers.
- Installation: Download and install the latest LTS version from nodejs.org.
- Verification: Open your terminal or command prompt and run:You should see the installed versions.
node -v npm -v
Code Editor: A good code editor will significantly improve your development experience. Visual Studio Code (VS Code) is highly recommended.
- Installation: Download and install from code.visualstudio.com.
- Recommended Extensions: For JavaScript/React development, consider extensions like ESLint, Prettier, and various React snippets.
Terminal/Command Prompt: You’ll use your terminal to run commands, create projects, and start development servers.
Initial Project Setup (Monorepo Approach with npm workspaces):
For learning Module Federation, it’s often easiest to start with a monorepo structure. This allows you to manage multiple interdependent applications within a single repository.
Let’s create a basic monorepo using npm workspaces.
Create a Root Directory:
mkdir module-federation-tutorial cd module-federation-tutorialInitialize
npmMonorepo: Create apackage.jsonfile at the root. You can do this manually or by runningnpm initand answering the prompts. The most important part is adding the"workspaces"field.// package.json at the root { "name": "module-federation-monorepo", "version": "1.0.0", "private": true, "workspaces": [ "packages/*" ], "scripts": { "start:all": "npm run --workspaces start" } }"private": true: Prevents the root package from being published to an npm registry."workspaces": ["packages/*"]: Tellsnpmto look for packages (your micro-frontends) inside thepackagesdirectory."start:all": A convenient script to start all applications in your workspaces (we’ll definestartscripts in individual apppackage.jsonfiles later).
Create the
packagesDirectory:mkdir packages
Now your basic monorepo is set up! We’ll create our host and remote applications inside the packages directory.
2. Core Concepts and Fundamentals
In this section, we’ll dive into the fundamental building blocks of Module Federation. We’ll start with the two main roles an application can play: Host and Remote, and then explore how they interact through exposed and shared modules.
2.1 Host and Remote Applications
At its heart, Module Federation involves at least two types of applications:
- Host Application (Container/Shell): This is the application that loads and orchestrates modules from other applications. It acts as the “shell” or “container” for your micro-frontends. It defines which remote applications it wants to consume.
- Remote Application (Micro-Frontend): This is an independent application that “exposes” some of its code (components, functions, etc.) to be consumed by other applications. It can also act as a host itself, consuming modules from other remotes (bidirectional hosting).
Analogy: Think of a magazine. The Host Application is the magazine itself, providing the main structure, layout, and overall theme. The Remote Applications are individual articles or sections written by different authors. The magazine (host) “loads” these articles (remotes) into its pages, and the articles (remotes) can also include snippets or references from other articles.
Setting up our first Host and Remote (using React and Webpack)
Let’s create two simple React applications: a host-app and a remote-app-1.
1. Create host-app:
Navigate to the packages directory and create a new React app. We’ll use create-react-app for simplicity, but you could also set up a React app manually.
cd packages
npx create-react-app host-app --template typescript
cd host-app
Now, we need to modify the webpack.config.js for Module Federation. create-react-app hides its Webpack configuration, so we’ll “eject” it or use a library that allows custom Webpack configuration without ejecting. For a beginner-friendly approach, let’s keep it simple and directly configure Webpack without ejecting, which means we’ll install Webpack and its plugins manually for this example.
Important Note for create-react-app: Ejecting create-react-app (npm run eject) reveals the underlying Webpack config, but it’s a one-way operation. For our tutorial, we’ll simulate a custom Webpack setup from scratch in our packages to make the configuration explicit, as create-react-app defaults do not expose Module Federation.
Let’s start over with a minimal Webpack setup for both host and remote.
Revisiting Setup: Minimal Webpack Project
Let’s go back to our module-federation-tutorial/packages directory. We’ll create two plain React projects without create-react-app to have full control over Webpack.
1. Initialize host-app:
cd module-federation-tutorial/packages
mkdir host-app
cd host-app
npm init -y
npm install react react-dom webpack webpack-cli webpack-dev-server html-webpack-plugin babel-loader @babel/core @babel/preset-react --save-dev
npm install --save-dev typescript @types/react @types/react-dom @types/webpack @types/webpack-dev-server
Create an src directory and an index.tsx file inside host-app/src:
// host-app/src/index.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
const App = () => {
return (
<div>
<h1>Host Application</h1>
{/* Remote component will be loaded here */}
</div>
);
};
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
root.render(<App />);
Create an index.html file inside host-app/public:
<!-- host-app/public/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Host App</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
Create host-app/webpack.config.js:
// host-app/webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { ModuleFederationPlugin } = require('webpack').container;
const path = require('path');
module.exports = {
entry: './src/index.tsx',
mode: 'development',
devServer: {
port: 3000,
historyApiFallback: true, // For React Router
},
resolve: {
extensions: ['.tsx', '.ts', '.js'],
},
module: {
rules: [
{
test: /\.(js|jsx|ts|tsx)$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-react', '@babel/preset-typescript'],
},
},
},
],
},
plugins: [
new ModuleFederationPlugin({
name: 'hostApp',
remotes: {
// We'll define remotes here later
// remoteApp1: 'remoteApp1@http://localhost:3001/remoteEntry.js',
},
shared: {
react: { singleton: true, requiredVersion: '^18.2.0' },
'react-dom': { singleton: true, requiredVersion: '^18.2.0' },
},
}),
new HtmlWebpackPlugin({
template: './public/index.html',
}),
],
output: {
publicPath: 'http://localhost:3000/', // Important for Module Federation
},
};
Add a start script to host-app/package.json:
// host-app/package.json
{
"name": "host-app",
"version": "1.0.0",
"scripts": {
"start": "webpack serve --open"
},
"devDependencies": {
"@babel/core": "^7.20.0",
"@babel/preset-react": "^7.20.0",
"@babel/preset-typescript": "^7.20.0",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"@types/webpack": "^5.28.0",
"@types/webpack-dev-server": "^4.7.0",
"babel-loader": "^9.1.0",
"html-webpack-plugin": "^5.5.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"typescript": "^5.0.0",
"webpack": "^5.75.0",
"webpack-cli": "^5.0.0",
"webpack-dev-server": "^4.11.0"
}
}
2. Initialize remote-app-1:
Go back to the packages directory and create remote-app-1.
cd ../
mkdir remote-app-1
cd remote-app-1
npm init -y
npm install react react-dom webpack webpack-cli webpack-dev-server html-webpack-plugin babel-loader @babel/core @babel/preset-react --save-dev
npm install --save-dev typescript @types/react @types/react-dom @types/webpack @types/webpack-dev-server
Create an src directory and an index.tsx file inside remote-app-1/src:
// remote-app-1/src/index.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import MyButton from './MyButton';
const App = () => {
return (
<div>
<h1>Remote App 1</h1>
<MyButton />
</div>
);
};
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
root.render(<App />);
Create remote-app-1/src/MyButton.tsx (this will be our exposed component):
// remote-app-1/src/MyButton.tsx
import React from 'react';
const MyButton: React.FC = () => {
const handleClick = () => {
alert('Hello from Remote App 1!');
};
return (
<button onClick={handleClick} style={{ padding: '10px', backgroundColor: 'lightblue', border: 'none', borderRadius: '5px', cursor: 'pointer' }}>
Click Me (Remote 1 Button)
</button>
);
};
export default MyButton;
Create an index.html file inside remote-app-1/public:
<!-- remote-app-1/public/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Remote App 1</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
Create remote-app-1/webpack.config.js:
// remote-app-1/webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { ModuleFederationPlugin } = require('webpack').container;
const path = require('path');
module.exports = {
entry: './src/index.tsx',
mode: 'development',
devServer: {
port: 3001, // Different port for remote app
historyApiFallback: true,
},
resolve: {
extensions: ['.tsx', '.ts', '.js'],
},
module: {
rules: [
{
test: /\.(js|jsx|ts|tsx)$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-react', '@babel/preset-typescript'],
},
},
},
],
},
plugins: [
new ModuleFederationPlugin({
name: 'remoteApp1',
filename: 'remoteEntry.js', // The file that describes the exposed modules
exposes: {
'./MyButton': './src/MyButton', // Expose MyButton component
},
shared: {
react: { singleton: true, requiredVersion: '^18.2.0' },
'react-dom': { singleton: true, requiredVersion: '^18.2.0' },
},
}),
new HtmlWebpackPlugin({
template: './public/index.html',
}),
],
output: {
publicPath: 'http://localhost:3001/', // Important for Module Federation
},
};
Add a start script to remote-app-1/package.json:
// remote-app-1/package.json
{
"name": "remote-app-1",
"version": "1.0.0",
"scripts": {
"start": "webpack serve --open"
},
"devDependencies": {
"@babel/core": "^7.20.0",
"@babel/preset-react": "^7.20.0",
"@babel/preset-typescript": "^7.20.0",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"@types/webpack": "^5.28.0",
"@types/webpack-dev-server": "^4.7.0",
"babel-loader": "^9.1.0",
"html-webpack-plugin": "^5.5.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"typescript": "^5.0.0",
"webpack": "^5.75.0",
"webpack-cli": "^5.0.0",
"webpack-dev-server": "^4.11.0"
}
}
Now, we need to link remote-app-1 to host-app.
Update host-app/webpack.config.js to consume remote-app-1:
Modify the remotes section:
// host-app/webpack.config.js
// ... (rest of the file)
plugins: [
new ModuleFederationPlugin({
name: 'hostApp',
remotes: {
remoteApp1: 'remoteApp1@http://localhost:3001/remoteEntry.js', // Define remote
},
shared: {
react: { singleton: true, requiredVersion: '^18.2.0' },
'react-dom': { singleton: true, requiredVersion: '^18.2.0' },
},
}),
new HtmlWebpackPlugin({
template: './public/index.html',
}),
],
// ... (rest of the file)
Update host-app/src/index.tsx to use the remote component:
// host-app/src/index.tsx
import React, { Suspense, lazy } from 'react';
import ReactDOM from 'react-dom/client';
// Dynamically import the remote component
const RemoteButton = lazy(() => import('remoteApp1/MyButton'));
const App = () => {
return (
<div>
<h1>Host Application</h1>
<h2>Loading Remote Component:</h2>
<Suspense fallback={<div>Loading Remote Button...</div>}>
<RemoteButton />
</Suspense>
</div>
);
};
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
root.render(<App />);
Running the Applications:
Open two separate terminal windows.
Terminal 1 (for remote-app-1):
cd module-federation-tutorial/packages/remote-app-1
npm start
This will start remote-app-1 on http://localhost:3001. You should see “Remote App 1” and the “Click Me (Remote 1 Button)” button.
Terminal 2 (for host-app):
cd module-federation-tutorial/packages/host-app
npm start
This will start host-app on http://localhost:3000. You should see “Host Application”, “Loading Remote Component:”, and after a brief moment, the “Click Me (Remote 1 Button)” button will appear, indicating that the component was successfully loaded from remote-app-1.
Explanation:
ModuleFederationPlugin: This is the core Webpack plugin that enables Module Federation.name: A unique identifier for the current application (e.g.,hostApp,remoteApp1).filename(for Remotes): Specifies the name of the bundle file that will expose modules (remoteEntry.js). This file acts as an entry point for other applications to discover and load modules.exposes(for Remotes): An object where keys are the public names of the modules you want to expose, and values are the paths to those modules within the remote application. For example,'./MyButton': './src/MyButton'meansMyButtonfromsrc/MyButton.tsxcan be imported asremoteApp1/MyButton.remotes(for Hosts): An object where keys are the local names you’ll use to import remote modules, and values are thenameof the remote application followed by itspublicPathandfilename. For example,remoteApp1: 'remoteApp1@http://localhost:3001/remoteEntry.js'means thehostAppcan now import modules fromremoteApp1usingremoteApp1/....shared: This is crucial for performance and preventing version conflicts. It specifies libraries that should be shared between federated applications.singleton: true: Ensures that only one instance of the shared module is loaded, even if multiple remotes (or the host) require it. This is essential for libraries like React to avoid issues with contexts, hooks, and state.requiredVersion: '^18.2.0': Specifies the acceptable version range for the shared dependency. If a remote requires a different version, Module Federation attempts to reconcile them, prioritizing the host’s version or a compatible one.
publicPathinoutput: This is critical for Module Federation. It tells Webpack where to find the bundled assets for the application, especiallyremoteEntry.js. It must be an absolute URL for cross-application loading.lazyandSuspense: When consuming remote components, it’s best practice to useReact.lazyfor code-splitting andReact.Suspenseto provide a fallback (like a loading indicator) while the remote module is being fetched.
Exercise 2.1: Create another Remote Application
- Create a new application named
remote-app-2inside thepackagesdirectory. - Install all necessary dependencies, similar to
remote-app-1. - Configure its
webpack.config.jsto run on port3002. - Create a component named
MyHeaderinremote-app-2/src/MyHeader.tsxthat displays a simple header (e.g., “Hello from Remote App 2 Header!”). - Expose
MyHeaderinremote-app-2’s Module Federation configuration. - Modify the
host-app/webpack.config.jsto includeremote-app-2in itsremotesconfiguration. - Modify
host-app/src/index.tsxto dynamically load and displayMyHeaderfromremote-app-2below theRemoteButton. - Run all three applications and verify that both remote components are displayed in the host application.
2.2 Exposing and Consuming Modules
As seen in the previous section, the core mechanism of Module Federation revolves around:
- Exposing Modules (from Remote): A remote application explicitly defines which of its internal modules (components, utilities, data, etc.) it wants to make available to other applications. This is done via the
exposesproperty in theModuleFederationPlugin. - Consuming Modules (in Host): A host application declares which remote applications it intends to use and how to locate their
remoteEntry.jsfile. This is done via theremotesproperty in theModuleFederationPlugin. Once configured, the host canimportthese remote modules as if they were local.
Detailed Explanation:
Exposing: When you expose a module, you give it an “internal” name within the federated system that other applications will use.
// remote-app/webpack.config.js
// ...
plugins: [
new ModuleFederationPlugin({
name: 'remoteApp',
filename: 'remoteEntry.js',
exposes: {
'./MyComponent': './src/components/MyComponent', // Exposing a React component
'./utils': './src/utils/math', // Exposing a utility file
'./store': './src/store/index.ts', // Exposing a state store
},
// ...
}),
],
// ...
Here, remoteApp/MyComponent, remoteApp/utils, and remoteApp/store become public interfaces for other applications to use.
Consuming:
A host application references the exposed modules by prefixing the remote application’s declared name (from its remotes config) with the exposed module’s alias.
// host-app/src/App.tsx
import React, { lazy, Suspense } from 'react';
const RemoteMyComponent = lazy(() => import('remoteApp/MyComponent'));
const { add } = await import('remoteApp/utils'); // For non-React modules, dynamic import can be direct
const App = () => {
const sum = add(5, 3);
return (
<div>
<h1>Host App</h1>
<Suspense fallback={<div>Loading Remote Component...</div>}>
<RemoteMyComponent />
</Suspense>
<p>Result from remote util: {sum}</p>
</div>
);
};
Notice how import('remoteApp/MyComponent') and import('remoteApp/utils') work. The remoteApp part comes from the key defined in the host’s remotes configuration, and MyComponent or utils comes from the exposes configuration of the remoteApp.
2.3 Shared Modules and Dependency Management
One of the most significant advantages of Module Federation is its intelligent dependency sharing mechanism. In a micro-frontend architecture, it’s very common for multiple applications to depend on the same libraries (e.g., React, Redux, Material-UI). Without proper sharing, each application would bundle its own copy of these libraries, leading to:
- Increased Bundle Sizes: Larger downloads for the user.
- Performance Issues: Redundant loading and execution of the same code.
- Runtime Errors: Especially with libraries like React, having multiple instances on the page can lead to context breaks, hook issues, and unexpected behavior.
The shared property in the ModuleFederationPlugin addresses these issues.
Detailed Explanation of shared configuration:
// Example shared configuration
shared: {
react: {
singleton: true,
requiredVersion: '^18.2.0', // Host will try to use its own version within this range
eager: false, // Load lazily by default
},
'react-dom': {
singleton: true,
requiredVersion: '^18.2.0',
eager: false,
},
lodash: {
singleton: true,
requiredVersion: '^4.17.21',
// No explicit eager, defaults to lazy
},
'@my-org/design-system': { // Sharing a custom design system library
singleton: true,
requiredVersion: '^1.0.0',
}
}
Key properties within shared:
singleton: true: This is paramount for libraries that should only have one instance across all federated applications loaded on the page. React is the prime example. If set totrue, Module Federation will ensure that if the host and a remote both depend on React, only one copy (usually the host’s version, if compatible) is loaded and used by both.requiredVersion: Specifies a version range (using semver) that the application is compatible with. Module Federation will attempt to use a shared module if its version falls within this range. If a conflicting version is found, the system tries to find the highest compatible version.strictVersion: true(optional): If set totruewithrequiredVersion, Module Federation will throw an error if the host and remote cannot agree on a compatible version. This ensures strict version control but can be less forgiving.
eager: true(optional): By default, shared modules are loaded lazily (only when first requested). Settingeager: truemeans the shared module will be loaded immediately when the entry point of the application loads. Use with caution, as it can increase initial load times, but might be necessary for some core dependencies.import(optional): Allows you to specify the actual module to be loaded. Useful if thepackage.jsonname differs from the internal import path.shareKey(optional): Allows a different key to be used for sharing than the module name itself.
How Module Federation Resolves Shared Dependencies:
- When an application starts, it builds a graph of its own dependencies and the
shareddependencies it needs. - When a remote module is loaded, Module Federation compares its
shareddependencies with those already loaded by the host. - If a compatible version of a shared module is already loaded (and
singleton: trueis set), the remote will reuse it. - If a compatible version is not available, or if the
singletonrule isn’t met (e.g.,singleton: false), the remote might load its own copy or attempt to resolve a new shared version based onrequiredVersionandstrictVersionrules.
Exercise 2.2: Verify Shared Dependencies
- Ensure your
host-appandremote-app-1webpack.config.jsfiles have thesharedconfiguration forreactandreact-domwithsingleton: trueandrequiredVersion: '^18.2.0'. - Start both applications (
remote-app-1first, thenhost-app). - Open your browser’s developer tools (usually F12).
- Go to the “Network” tab.
- Filter by “JS” or “JavaScript”.
- You should observe that
react.jsandreact-dom.js(or their bundled equivalents) are loaded only once by either the host or the first remote that needs them, not by both independently. If you see two separate downloads for React, check yoursingletonandrequiredVersionconfigurations.
Mini-Challenge:
Imagine you have a utility-library micro-frontend that exposes a formatDate function.
- Create
remote-app-3that exposes this function. - Make sure
host-appconsumes thisformatDatefunction and displays the formatted date. - Ensure
remote-app-1also consumes and uses thisformatDatefunction if it needs to display a date. - How would you configure
sharedto make suremoment.js(if used byformatDate) is only loaded once?
3. Intermediate Topics
Now that you have a solid grasp of the foundational concepts, let’s explore some intermediate topics that enhance the flexibility and robustness of your Module Federation applications.
3.1 Dynamic Remote Loading and Entrypoint Configuration
In our initial setup, the remotes configuration in the host-app was static, meaning the host knew exactly where remoteApp1’s remoteEntry.js file was located at build time. While this works for simple cases, real-world micro-frontends often require more flexibility:
- Dynamic URLs: Remote applications might be deployed to different environments (dev, staging, prod) with varying URLs.
- Feature Flags: You might want to load different remotes based on feature flags or user permissions.
- API-driven Remotes: The list of available remotes and their entry points could come from a backend API.
Module Federation supports dynamic remote loading to handle these scenarios. Instead of a static string for the remote’s entry point, you can provide a function that returns a Promise resolving to the remote’s location.
Detailed Explanation:
1. Function as Remote Value:
You can specify a function that returns a Promise to the remote’s entry point.
// host-app/webpack.config.js
// ...
plugins: [
new ModuleFederationPlugin({
name: 'hostApp',
remotes: {
remoteApp1: `promise new Promise(resolve => {
const urlParams = new URLSearchParams(window.location.search);
const remoteUrl = urlParams.get('remoteApp1Url') || 'http://localhost:3001/remoteEntry.js';
resolve('remoteApp1@' + remoteUrl);
})`,
// Or from an environment variable
// remoteApp2: `remoteApp2@\${process.env.REMOTE_APP_2_URL}/remoteEntry.js`,
},
// ...
}),
],
// ...
In this example:
- The
remoteApp1entry is now apromisethat resolves to the actual remote URL. - It checks for a
remoteApp1Urlquery parameter, otherwise defaults tohttp://localhost:3001/remoteEntry.js. This allows you to dynamically control which version or instance ofremoteApp1the host loads by simply changing the URL in the browser (e.g.,http://localhost:3000/?remoteApp1Url=http://staging.remoteapp1.com/remoteEntry.js).
2. Asynchronous Remote Loading (Runtime):
For even greater flexibility, especially if you need to fetch remote URLs from an API, you can define an empty remotes object and dynamically load remotes at runtime using a helper function.
First, define an empty remotes in webpack.config.js:
// host-app/webpack.config.js
// ...
plugins: [
new ModuleFederationPlugin({
name: 'hostApp',
remotes: {}, // Empty remotes
shared: { /* ... */ },
}),
// ...
],
// ...
Then, in your host application’s JavaScript, you can programmatically load remotes:
// host-app/src/App.tsx
import React, { Suspense, lazy, useEffect, useState } from 'react';
import ReactDOM from 'react-dom/client';
// This function needs to be globally available or part of your build process
// In a real app, this might come from a configuration service or a shared utility.
declare const __webpack_init_sharing__: (shareScope: string) => Promise<void>;
declare const __webpack_share_scopes__: any;
declare const __webpack_remotes__: any;
interface RemoteConfig {
name: string;
url: string;
scope: string;
module: string;
}
const loadRemoteModule = async (remoteConfig: RemoteConfig) => {
if (!window[remoteConfig.scope]) {
await __webpack_init_sharing__('default'); // Initialize sharing
const container = new Promise<any>((resolve, reject) => {
const script = document.createElement('script');
script.src = remoteConfig.url;
script.onload = () => {
script.remove();
// @ts-ignore
if (!window[remoteConfig.scope]) { // Ensure the global scope is present
reject(new Error(`Failed to load remote: ${remoteConfig.name} - global scope not found.`));
return;
}
// @ts-ignore
window[remoteConfig.scope].init(__webpack_share_scopes__.default)
.then(() => resolve(window[remoteConfig.scope]))
.catch(reject);
};
script.onerror = reject;
document.head.appendChild(script);
});
window[remoteConfig.scope] = await container;
}
// @ts-ignore
const moduleFactory = await window[remoteConfig.scope].get(remoteConfig.module);
return moduleFactory();
};
const App = () => {
const [remoteButtonComponent, setRemoteButtonComponent] = useState<React.ComponentType | null>(null);
useEffect(() => {
const fetchRemoteConfig = async () => {
// In a real app, this could be an API call
const remoteConfigs: RemoteConfig[] = [
{
name: 'remoteApp1',
url: 'http://localhost:3001/remoteEntry.js',
scope: 'remoteApp1', // This must match the 'name' in remote's ModuleFederationPlugin
module: './MyButton',
},
// Add other remotes dynamically
];
try {
const buttonModule = await loadRemoteModule(remoteConfigs[0]);
setRemoteButtonComponent(() => buttonModule.default); // Assuming it's a default export
} catch (error) {
console.error('Failed to load remote component:', error);
// Handle error, e.g., display a fallback UI
}
};
fetchRemoteConfig();
}, []);
const RemoteButton = remoteButtonComponent
? lazy(() => Promise.resolve({ default: remoteButtonComponent }))
: null;
return (
<div>
<h1>Host Application</h1>
<h2>Loading Remote Component Dynamically:</h2>
{RemoteButton ? (
<Suspense fallback={<div>Loading Dynamic Remote Button...</div>}>
<RemoteButton />
</Suspense>
) : (
<div>Failed to load remote button or loading...</div>
)}
</div>
);
};
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
root.render(<App />);
This is a more advanced pattern and involves a deeper understanding of the Webpack runtime. The window[remoteConfig.scope].init(__webpack_share_scopes__.default) part is crucial for making sure shared modules are correctly initialized and reused.
Exercise 3.1: Implement Dynamic Remote Loading with Environment Variables
- In
host-app/webpack.config.js, modify theremoteApp1entry to use an environment variable.// host-app/webpack.config.js // ... plugins: [ new ModuleFederationPlugin({ name: 'hostApp', remotes: { remoteApp1: `remoteApp1@\${process.env.REMOTE_APP_1_URL || 'http://localhost:3001/remoteEntry.js'}`, }, // ... }), ], // ... - Add a
DefinePluginto yourhost-app/webpack.config.jsto inject environment variables:// host-app/webpack.config.js // ... const webpack = require('webpack'); // Add this line // ... plugins: [ // ... new webpack.DefinePlugin({ 'process.env.REMOTE_APP_1_URL': JSON.stringify(process.env.REMOTE_APP_1_URL), }), ], // ... - Modify your
host-app/package.jsonstartscript to pass an environment variable:(Note: For cross-platform compatibility, consider using// host-app/package.json "scripts": { "start": "REMOTE_APP_1_URL=http://localhost:3001/remoteEntry.js webpack serve --open" },cross-envpackage:npm install --save-dev cross-envand thencross-env REMOTE_APP_1_URL=... webpack serve --open) - Run
remote-app-1on port 3001. - Run
host-app. Verify it still loadsremoteApp1. - Try running
host-appwithout theREMOTE_APP_1_URLenvironment variable. It should fall back to the defaulthttp://localhost:3001/remoteEntry.js.
3.2 Bidirectional Hosting
Module Federation is not limited to a one-way street where a host consumes remotes. Applications can act as both a host and a remote simultaneously. This is called bidirectional hosting.
Detailed Explanation:
Consider a scenario where:
App AexposesComponent A.App BexposesComponent B.App Cis a shell that consumes bothComponent AandComponent B.- But also,
App Amight need a utility function exposed byApp B.
In such a case, App A would be a remote to App C, but also a host to App B. Similarly, App B would be a remote to App C, and also a remote to App A (if App A consumes something from App B).
Configuration for Bidirectional Hosting:
The setup is straightforward: an application simply includes both remotes and exposes in its ModuleFederationPlugin configuration.
Example Scenario:
remote-app-1(running onhttp://localhost:3001) exposesMyButton.remote-app-2(running onhttp://localhost:3002) exposesMyHeaderand needs to consumeMyButtonfromremote-app-1.
1. remote-app-1 (Already set up to expose MyButton):
// remote-app-1/webpack.config.js
// ...
plugins: [
new ModuleFederationPlugin({
name: 'remoteApp1',
filename: 'remoteEntry.js',
exposes: {
'./MyButton': './src/MyButton',
},
shared: { /* ... */ },
}),
],
// ...
2. remote-app-2 (Will act as both remote and host):
cd module-federation-tutorial/packages
mkdir remote-app-2
cd remote-app-2
npm init -y
npm install react react-dom webpack webpack-cli webpack-dev-server html-webpack-plugin babel-loader @babel/core @babel/preset-react --save-dev
npm install --save-dev typescript @types/react @types/react-dom @types/webpack @types/webpack-dev-server
Create remote-app-2/src/index.tsx:
// remote-app-2/src/index.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import MyHeader from './MyHeader';
const App = () => {
return (
<div>
<MyHeader />
</div>
);
};
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
root.render(<App />);
Create remote-app-2/src/MyHeader.tsx (exposes this):
// remote-app-2/src/MyHeader.tsx
import React, { lazy, Suspense } from 'react';
// This remote will also consume a component from remoteApp1
const RemoteButton = lazy(() => import('remoteApp1/MyButton'));
const MyHeader: React.FC = () => {
return (
<div style={{ padding: '15px', backgroundColor: '#f0f0f0', borderBottom: '1px solid #ccc' }}>
<h3>Header from Remote App 2</h3>
<p>This header also consumes a button from Remote App 1:</p>
<Suspense fallback={<div>Loading Remote Button in Header...</div>}>
<RemoteButton />
</Suspense>
</div>
);
};
export default MyHeader;
Create remote-app-2/public/index.html:
<!-- remote-app-2/public/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Remote App 2</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
Create remote-app-2/webpack.config.js:
// remote-app-2/webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { ModuleFederationPlugin } = require('webpack').container;
const path = require('path');
module.exports = {
entry: './src/index.tsx',
mode: 'development',
devServer: {
port: 3002, // Different port
historyApiFallback: true,
},
resolve: {
extensions: ['.tsx', '.ts', '.js'],
},
module: {
rules: [
{
test: /\.(js|jsx|ts|tsx)$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-react', '@babel/preset-typescript'],
},
},
},
],
},
plugins: [
new ModuleFederationPlugin({
name: 'remoteApp2',
filename: 'remoteEntry.js',
exposes: {
'./MyHeader': './src/MyHeader', // Exposing MyHeader
},
remotes: {
remoteApp1: 'remoteApp1@http://localhost:3001/remoteEntry.js', // Consuming RemoteApp1
},
shared: {
react: { singleton: true, requiredVersion: '^18.2.0' },
'react-dom': { singleton: true, requiredVersion: '^18.2.0' },
},
}),
new HtmlWebpackPlugin({
template: './public/index.html',
}),
],
output: {
publicPath: 'http://localhost:3002/',
},
};
Add start script to remote-app-2/package.json:
// remote-app-2/package.json
{
"name": "remote-app-2",
"version": "1.0.0",
"scripts": {
"start": "webpack serve --open"
},
"devDependencies": {
"@babel/core": "^7.20.0",
"@babel/preset-react": "^7.20.0",
"@babel/preset-typescript": "^7.20.0",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"@types/webpack": "^5.28.0",
"@types/webpack-dev-server": "^4.7.0",
"babel-loader": "^9.1.0",
"html-webpack-plugin": "^5.5.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"typescript": "^5.0.0",
"webpack": "^5.75.0",
"webpack-cli": "^5.0.0",
"webpack-dev-server": "^4.11.0"
}
}
3. Update host-app to consume MyHeader from remote-app-2:
// host-app/webpack.config.js
// ...
plugins: [
new ModuleFederationPlugin({
name: 'hostApp',
remotes: {
remoteApp1: 'remoteApp1@http://localhost:3001/remoteEntry.js',
remoteApp2: 'remoteApp2@http://localhost:3002/remoteEntry.js', // Add remoteApp2
},
shared: { /* ... */ },
}),
// ...
],
// ...
// host-app/src/index.tsx
import React, { Suspense, lazy } from 'react';
import ReactDOM from 'react-dom/client';
const RemoteButton = lazy(() => import('remoteApp1/MyButton'));
const RemoteHeader = lazy(() => import('remoteApp2/MyHeader')); // Import RemoteHeader
const App = () => {
return (
<div>
<h1>Host Application</h1>
<h2>Component from Remote App 1:</h2>
<Suspense fallback={<div>Loading Remote Button...</div>}>
<RemoteButton />
</Suspense>
<h2>Component from Remote App 2 (which also uses Remote App 1's Button):</h2>
<Suspense fallback={<div>Loading Remote Header...</div>}>
<RemoteHeader />
</Suspense>
</div>
);
};
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
root.render(<App />);
Running Bidirectional Hosting Example:
- Open three terminal windows.
- Terminal 1 (for
remote-app-1):(Runs oncd module-federation-tutorial/packages/remote-app-1 npm starthttp://localhost:3001) - Terminal 2 (for
remote-app-2):(Runs oncd module-federation-tutorial/packages/remote-app-2 npm starthttp://localhost:3002. You’ll see “Header from Remote App 2” and inside it, “Click Me (Remote 1 Button)” button. This confirmsremote-app-2is consumingremote-app-1’s button). - Terminal 3 (for
host-app):(Runs oncd module-federation-tutorial/packages/host-app npm starthttp://localhost:3000. You’ll see “Host Application”, “Component from Remote App 1:” with the button, and “Component from Remote App 2…” with the header, which itself contains the button. This demonstrateshost-appconsuming both remotes, andremote-app-2consumingremote-app-1).
This setup elegantly demonstrates how different parts of your application can interact in a distributed fashion. The shared dependencies (React, React-DOM) are still managed efficiently, ensuring only one instance is loaded.
Mini-Challenge:
Modify remote-app-1 to consume a “Footer” component exposed by remote-app-2. How would you update the remote-app-1/webpack.config.js and remote-app-1/src/index.tsx files to achieve this?
3.3 Versioning Strategies
Managing dependencies and their versions across multiple independent micro-frontends is one of the more complex aspects of Module Federation. Without careful planning, version conflicts can lead to broken applications or unexpected behavior. Module Federation provides tools to help manage this.
Detailed Explanation:
We’ve already touched upon requiredVersion and singleton in the shared configuration. Let’s delve deeper into how these and other strategies help.
1. requiredVersion and singleton:
requiredVersion: '^18.2.0': This tells Module Federation that the application (host or remote) is compatible with any version of the dependency starting from18.2.0up to19.0.0(but not including).singleton: true: When true, Module Federation attempts to load only one instance of this module. It prefers the host’s version if compatible. If the host doesn’t provide it, or if its version isn’t compatible, it will try to find a compatible version among the remotes. If no compatible version can be found andstrictVersionis nottrue, it might load multiple versions as a fallback.
2. strictVersion: true (for shared dependencies):
When
strictVersion: trueis combined withrequiredVersion, Module Federation becomes very strict. If a host and a remote declare conflictingrequiredVersionranges that have no overlap, or if a compatiblesingletoncannot be established, the build (or runtime) will fail. This is useful for preventing hard-to-debug runtime issues but requires careful coordination between teams.// shared with strict versioning shared: { react: { singleton: true, requiredVersion: '^18.2.0', strictVersion: true, // Will fail if incompatible version is loaded }, }
3. version (for shared dependencies):
- If you explicitly want a specific application to provide a certain version for sharing, you can use
version. This is less commonly used thanrequiredVersionbut can be useful in specific scenarios.// host-app providing a specific version shared: { react: { singleton: true, version: '18.2.0', // Host explicitly provides 18.2.0 }, }
4. Fallbacks:
Module Federation supports fallbacks when a remote module cannot be loaded (e.g., due to network issues, a non-existent remote, or version incompatibility). You can define a local fallback module that will be used instead. This adds resilience to your application.
// Example in host-app's ModuleFederationPlugin remotes config remotes: { remoteApp1: `remoteApp1@http://localhost:3001/remoteEntry.js || ./src/fallbacks/RemoteApp1Fallback`, },And then create
host-app/src/fallbacks/RemoteApp1Fallback.tsx:// host-app/src/fallbacks/RemoteApp1Fallback.tsx import React from 'react'; const RemoteApp1Fallback: React.FC = () => { return ( <div style={{ border: '1px solid red', padding: '10px', color: 'red' }}> Error: Could not load Remote App 1. Displaying fallback UI. </div> ); }; export default RemoteApp1Fallback;Now, if
http://localhost:3001/remoteEntry.jsis unreachable or fails to load, the fallback component will be used.
5. Semantic Versioning and Communication:
- Beyond technical configurations, effective communication and adherence to semantic versioning (SemVer) are paramount for successful micro-frontend development.
- SemVer: Micro-frontend teams should clearly communicate breaking changes (major version bumps), new features (minor version bumps), and bug fixes (patch version bumps).
- API Contracts: Define clear API contracts for exposed modules (inputs, outputs, events) to minimize unexpected breaking changes.
- Shared Design System: A unified design system helps ensure visual consistency and provides a common language for UI components, reducing the need to share every granular component via federation.
Exercise 3.3: Implement Fallback UI
- Modify
host-app/webpack.config.jsto include a fallback forremoteApp1.// host-app/webpack.config.js // ... plugins: [ new ModuleFederationPlugin({ name: 'hostApp', remotes: { remoteApp1: `remoteApp1@http://localhost:3001/remoteEntry.js || ./src/fallbacks/RemoteApp1Fallback`, remoteApp2: 'remoteApp2@http://localhost:3002/remoteEntry.js', }, shared: { /* ... */ }, }), // ... ], // ... - Create the
host-app/src/fallbacksdirectory andRemoteApp1Fallback.tsxfile as described above. - Start
host-appandremote-app-2. Do NOT startremote-app-1. - Navigate to
http://localhost:3000. You should see the fallback UI forremoteApp1and theMyHeaderfromremoteApp2. This demonstrates graceful degradation.
Mini-Challenge:
How would you implement a mechanism to allow a remote application to prefer its own version of a shared library (e.g., lodash) if the host’s version is too old, but still use the host’s newer version if available? (Hint: look into shareConfig options like version and import).
4. Advanced Topics and Best Practices
Having covered the fundamentals and intermediate concepts, let’s explore more advanced aspects of Module Federation, focusing on real-world considerations, performance, and best practices.
4.1 Advanced Configuration with Different Environments
Deploying micro-frontends involves managing different environments (development, staging, production), each often having unique URLs for remote entry points. We touched on dynamic remote loading with environment variables, but let’s formalize this with dedicated configuration.
Detailed Explanation:
The most common approach is to use environment variables or a configuration file that is specific to each environment.
1. Using Environment Variables and webpack.DefinePlugin (Revisited):
This is a standard Webpack practice to inject global variables into your client-side code.
// webpack.config.js (for host or remote)
const webpack = require('webpack');
const { ModuleFederationPlugin } = require('webpack').container;
module.exports = (env, argv) => {
const isProduction = argv.mode === 'production';
return {
// ... other webpack config
plugins: [
new ModuleFederationPlugin({
name: 'hostApp',
remotes: {
remoteApp1: `remoteApp1@\${isProduction ? 'https://prod.remoteapp1.com' : 'http://localhost:3001'}/remoteEntry.js`,
// Or from a specific env variable
remoteApp2: `remoteApp2@\${process.env.REMOTE_APP_2_URL || 'http://localhost:3002'}/remoteEntry.js`,
},
shared: { /* ... */ },
}),
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(argv.mode),
'process.env.REMOTE_APP_2_URL': JSON.stringify(process.env.REMOTE_APP_2_URL), // Inject specific env var
}),
// ... other plugins
],
// ...
};
};
argv.modegives you the current webpack mode (development,production).process.env.REMOTE_APP_X_URLallows you to set specific URLs via command line for different builds.- Example for
package.jsonscripts:"scripts": { "start": "webpack serve --mode development", "build:dev": "REMOTE_APP_2_URL=http://staging-remote2.com webpack build --mode development", "build:prod": "REMOTE_APP_2_URL=https://prod-remote2.com webpack build --mode production" }
- Example for
2. Centralized Configuration Service: For very large micro-frontend ecosystems, storing remote URLs and other configurations in a central configuration service (e.g., a simple JSON hosted on a CDN, or a more sophisticated backend service) is a robust solution.
The host application would fetch this configuration at runtime.
This approach uses the dynamic remote loading technique discussed in section 3.1.
// host-app/src/bootstrap.tsx (or similar entry file) import { startApp } from './app'; async function loadConfigAndStartApp() { try { const configResponse = await fetch('/api/microfrontend-config'); // Fetch from your backend const config = await configResponse.json(); // Dynamically configure remotes based on fetched config // This is pseudo-code as direct modification of ModuleFederationPlugin at runtime is not straightforward // Instead, the `loadRemoteModule` function from 3.1 would use this config. // Example: If config structure is { remoteApp1: { url: "...", scope: "..." } } // Pass this config to a context provider or a global state for your app components to use. startApp(config); } catch (error) { console.error('Failed to load micro-frontend config:', error); // Render a full fallback page if critical config cannot be loaded } } loadConfigAndStartApp();
4.2 Performance Considerations and Optimization Strategies
While Module Federation offers many benefits, improper implementation can lead to performance bottlenecks. Optimizing your federated applications is crucial.
Detailed Explanation:
1. Lazy Loading (Default and Explicit):
Module Federation is lazy by default: Remote modules are not downloaded until they are actually imported and used. This is a massive performance win.
React.lazyandSuspense: Always wrap your dynamically imported React components withlazyandSuspensein your host application. This ensures that the component’s code is fetched only when it’s rendered, and provides a smooth user experience with loading indicators.import { lazy, Suspense } from 'react'; const RemoteComponent = lazy(() => import('remoteApp/Component')); function App() { return ( <Suspense fallback={<div>Loading Remote Component...</div>}> <RemoteComponent /> </Suspense> ); }
2. Effective Shared Dependency Management:
singleton: true: As discussed, this is critical to prevent multiple downloads and instances of core libraries like React, React-DOM, Redux, etc.requiredVersion: Use this to define compatible version ranges.- Avoid
eager: trueunless necessary: Eagerly loaded shared modules are bundled with the entry point, increasing initial load time. Only use it for critical dependencies that must be available immediately. - Minimize shared dependencies: Only share libraries that are truly common and large. For small, application-specific utilities, it might be better for each remote to bundle its own copy.
3. Code Splitting:
- Beyond Module Federation’s lazy loading, apply standard Webpack code-splitting techniques within your individual host and remote applications. This ensures that even within a single micro-frontend, code is split into smaller chunks and loaded on demand (e.g., routing-based code splitting).
4. Caching Strategies:
- Long-term caching for
remoteEntry.jsand federated chunks: Ensure your server/CDN serves these files with appropriateCache-Controlheaders (e.g.,max-age=31536000, immutable). - Cache busting: Module Federation often generates content hashes in filenames (
remoteEntry.jsbecomesremoteEntry.[hash].jsin production builds) which automatically handle cache busting. ForremoteEntry.jsitself, ensure its URL changes if the exposed modules change. You might need to configure your CI/CD to update theremoteEntry.jspath in the host’s configuration when a remote is updated. - Manifest Protocol (Module Federation 2.0): The
mf-manifest.jsonfile in MF 2.0 can simplify deployment platform integration and management of version resources and gray releases. This manifest provides metadata about the federated application.
5. HTTP/2 and CDNs:
- Deploy your micro-frontends to a Content Delivery Network (CDN) to reduce latency.
- Leverage HTTP/2 for efficient parallel loading of multiple smaller chunks, which is common in federated applications.
6. Build Optimization:
- Tree Shaking: Ensure your Webpack configuration is set up for tree-shaking to remove unused code from bundles.
- Minification & Compression: Always minify and compress your bundles (
gzip,brotli) for production.
7. Monitoring and Observability:
- Implement robust monitoring for each micro-frontend to track performance metrics, error rates, and resource usage. Tools like Webpack Bundle Analyzer can help identify large bundles or shared dependencies that are not being efficiently deduplicated.
4.3 Common Pitfalls and Debugging Techniques
Module Federation can introduce new complexities. Being aware of common pitfalls and having debugging strategies is essential.
Detailed Explanation:
Common Pitfalls:
Version Mismatches (Silent Failures or Runtime Errors):
- Problem: If
shareddependencies are not configured correctly orsingletonisn’t used for critical libraries, you might end up with multiple versions of the same library loaded. This can lead to cryptic errors, especially with React context or hooks. - Solution: Use
singleton: truefor frameworks and critical libraries. Be precise withrequiredVersion. UsestrictVersion: truein development to catch issues early. Establish clear communication between teams about shared dependency versions.
- Problem: If
publicPathIssues:- Problem: Incorrect
output.publicPathinwebpack.config.js(especially for remotes) can lead to bundles not being found (404 errors in the network tab). ThepublicPathmust be an absolute URL where the application’s assets are hosted. - Solution: Double-check that
publicPathis correctly configured for each application, including the trailing slash (e.g.,http://localhost:3001/).
- Problem: Incorrect
Missing
remoteEntry.jsor Exposed Modules:- Problem: If the host tries to load a remote that is not running, or if the
remoteEntry.jsfile is not accessible, or if an exposed module name is misspelled. - Solution: Verify that all remote applications are running on their configured ports. Check the network tab for 404s on
remoteEntry.jsor other federated chunks. Carefully inspectremotesandexposesnames for typos. Implement fallbacks as discussed earlier.
- Problem: If the host tries to load a remote that is not running, or if the
CSS Collisions/Global Styles:
- Problem: Micro-frontends often use different CSS methodologies. If not scoped properly, styles from one remote can unintentionally affect others or the host.
- Solution: Encourage isolated styling: CSS Modules, scoped CSS-in-JS (e.g., Emotion with unique class names), utility-first frameworks with explicit prefixes (e.g., Tailwind CSS with
prefix), or Web Components using Shadow DOM. A strong, unified design system implemented as a shared dependency can also mitigate this.
Performance Overheads (Initial Load Time, Waterfall Requests):
- Problem: Too many remotes, large shared bundles loaded eagerly, or inefficient network requests can slow down the application.
- Solution: Lazy load everything possible. Optimize shared dependencies. Leverage HTTP/2. Use Webpack Bundle Analyzer to identify large chunks.
Debugging Techniques:
Browser Developer Tools (Network Tab):
- Key tool: Monitor network requests. Look for 404 errors, observe which
remoteEntry.jsfiles are loaded, and check if shared dependencies are loaded once (and which application loaded them). - Resource Timing: Analyze the timing of module loads to identify bottlenecks.
- Key tool: Monitor network requests. Look for 404 errors, observe which
Console Logs:
- Add
console.logstatements in your host and remote applications to trace execution flow and verify that components are being loaded and rendered correctly. - Module Federation often provides useful warnings or errors in the console regarding shared dependency mismatches.
- Add
Webpack Stats and Bundle Analyzer:
- Generate Webpack stats (e.g.,
webpack --json > stats.json). - Use
webpack-bundle-analyzer(npm install --save-dev webpack-bundle-analyzer) to visualize your bundle composition, including shared modules and remote chunks. This helps identify large dependencies or inefficient sharing.
webpack --profile --json > stats.json webpack-bundle-analyzer stats.json- Generate Webpack stats (e.g.,
Chrome DevTools Extension (for Module Federation 2.0):
- Module Federation 2.0 comes with enhanced debugging tools, including a new Chrome DevTools extension. This can display dependencies between modules, exposed and shared configurations, and aid in debugging. Look for this in the Chrome Web Store.
window.__FEDERATION__(Development Tool):- In development, you can often inspect
window.__FEDERATION__in your browser’s console to see runtime information about loaded remotes and shared modules. This provides insights into how Module Federation is resolving dependencies.
- In development, you can often inspect
Error Boundaries (React-specific):
- Wrap dynamically loaded remote components with React Error Boundaries. This prevents a crashing remote from bringing down the entire host application, isolating failures and providing a user-friendly fallback.
class ErrorBoundary extends React.Component { state = { hasError: false }; static getDerivedStateFromError(error: any) { return { hasError: true }; } componentDidCatch(error: any, errorInfo: any) { console.error('ErrorBoundary caught an error:', error, errorInfo); } render() { if (this.state.hasError) { return <h1>Something went wrong loading this component.</h1>; } return this.props.children; } } // In your App: <Suspense fallback={<div>Loading...</div>}> <ErrorBoundary> <RemoteComponent /> </ErrorBoundary> </Suspense>
Exercise 4.1: Simulate a Remote App Failure and Observe Fallback/Error Boundary
- Ensure your
host-apphas the fallback configured forremoteApp1(from Exercise 3.3). - Also, wrap the
<RemoteButton />component inhost-app/src/index.tsxwith theErrorBoundarycomponent shown above. - Modify
remote-app-1/src/MyButton.tsxto intentionally throw an error (e.g.,throw new Error('Intentional error in remote button!');). - Start
remote-app-1andremote-app-2. - Start
host-app. - Observe
http://localhost:3000. What happens? Do you see the fallback UI, the error boundary message, or both? How does this impact the loading ofremote-app-2’s header? Analyze the console output. - Now, stop
remote-app-1. What happens to the host? Does the fallback take over?
5. Guided Projects
Learning by doing is the best way to master Module Federation. These projects will guide you through building practical micro-frontend applications.
Project 1: An E-commerce Product Listing Micro-Frontend
Objective: Build a simplified e-commerce platform where a central “Shell” application hosts a “Product List” micro-frontend and a “Shopping Cart” micro-frontend.
Technologies: React, Webpack 5, Module Federation.
Project Structure:
module-federation-tutorial/
├── packages/
│ ├── host-app/ (Shell application, port 3000)
│ ├── products-app/ (Remote: Displays product list, port 3001)
│ └── cart-app/ (Remote: Manages shopping cart, port 3002)
├── package.json (Monorepo root package.json)
Step-by-Step Guide:
Phase 1: Setup Host Application (host-app)
- Navigate to
packages:cd module-federation-tutorial/packages - Create
host-app: If you haven’t already, follow the “Setting up your first Host and Remote” section (starting from “Revisiting Setup: Minimal Webpack Project”) to createhost-app. Ensure it’s onport: 3000. - Update
host-app/src/index.tsx: Make it a basic shell for now.// host-app/src/index.tsx import React from 'react'; import ReactDOM from 'react-dom/client'; const App = () => { return ( <div> <nav style={{ padding: '10px', backgroundColor: '#333', color: 'white', display: 'flex', justifyContent: 'space-between' }}> <h1 style={{ margin: 0 }}>E-commerce Host</h1> <div> <a href="#" style={{ color: 'white', marginRight: '15px' }}>Home</a> <a href="#" style={{ color: 'white' }}>Cart (0)</a> {/* This will be updated by Cart App later */} </div> </nav> <div style={{ padding: '20px' }}> {/* Products App will go here */} {/* Cart App will go here */} </div> </div> ); }; const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement); root.render(<App />); host-app/webpack.config.js: ConfigureModuleFederationPluginwith a name, andsharedReact dependencies. Leaveremotesempty for now.// host-app/webpack.config.js const HtmlWebpackPlugin = require('html-webpack-plugin'); const { ModuleFederationPlugin } = require('webpack').container; const path = require('path'); module.exports = { entry: './src/index.tsx', mode: 'development', devServer: { port: 3000, historyApiFallback: true, }, resolve: { extensions: ['.tsx', '.ts', '.js'], }, module: { rules: [ { test: /\.(js|jsx|ts|tsx)$/, exclude: /node_modules/, use: { loader: 'babel-loader', options: { presets: ['@babel/preset-react', '@babel/preset-typescript'], }, }, }, ], }, plugins: [ new ModuleFederationPlugin({ name: 'hostApp', remotes: { // Will add productsApp and cartApp here }, shared: { react: { singleton: true, requiredVersion: '^18.2.0' }, 'react-dom': { singleton: true, requiredVersion: '^18.2.0' }, }, }), new HtmlWebpackPlugin({ template: './public/index.html', }), ], output: { publicPath: 'http://localhost:3000/', }, };- Test
host-app: Runcd host-app && npm start. You should see the basic E-commerce Host navigation bar.
Phase 2: Create Product List Micro-Frontend (products-app)
- Create
products-app:cd module-federation-tutorial/packages mkdir products-app cd products-app npm init -y npm install react react-dom webpack webpack-cli webpack-dev-server html-webpack-plugin babel-loader @babel/core @babel/preset-react --save-dev npm install --save-dev typescript @types/react @types/react-dom @types/webpack @types/webpack-dev-server products-app/public/index.html:<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Products App</title> </head> <body> <div id="root"></div> </body> </html>- Create
products-app/src/ProductList.tsx: This component will display a list of dummy products.// products-app/src/ProductList.tsx import React from 'react'; interface Product { id: number; name: string; price: number; } const products: Product[] = [ { id: 1, name: 'Laptop', price: 1200 }, { id: 2, name: 'Mouse', price: 25 }, { id: 3, name: 'Keyboard', price: 75 }, ]; const ProductList: React.FC = () => { return ( <div style={{ border: '1px solid #eee', padding: '20px', borderRadius: '8px', marginBottom: '20px' }}> <h2>Products Available</h2> <ul style={{ listStyle: 'none', padding: 0 }}> {products.map(product => ( <li key={product.id} style={{ marginBottom: '10px', padding: '10px', border: '1px solid #ddd', borderRadius: '5px', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}> <span>{product.name} - ${product.price}</span> <button style={{ padding: '8px 12px', backgroundColor: 'green', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer' }}> Add to Cart </button> </li> ))} </ul> </div> ); }; export default ProductList; products-app/src/index.tsx:// products-app/src/index.tsx import React from 'react'; import ReactDOM from 'react-dom/client'; import ProductList from './ProductList'; const App = () => { return ( <div> <ProductList /> </div> ); }; const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement); root.render(<App />);products-app/webpack.config.js: Configure it as a remote, exposingProductListon port3001.// products-app/webpack.config.js const HtmlWebpackPlugin = require('html-webpack-plugin'); const { ModuleFederationPlugin } = require('webpack').container; const path = require('path'); module.exports = { entry: './src/index.tsx', mode: 'development', devServer: { port: 3001, historyApiFallback: true, }, resolve: { extensions: ['.tsx', '.ts', '.js'], }, module: { rules: [ { test: /\.(js|jsx|ts|tsx)$/, exclude: /node_modules/, use: { loader: 'babel-loader', options: { presets: ['@babel/preset-react', '@babel/preset-typescript'], }, }, }, ], }, plugins: [ new ModuleFederationPlugin({ name: 'productsApp', filename: 'remoteEntry.js', exposes: { './ProductList': './src/ProductList', // Expose ProductList }, shared: { react: { singleton: true, requiredVersion: '^18.2.0' }, 'react-dom': { singleton: true, requiredVersion: '^18.2.0' }, }, }), new HtmlWebpackPlugin({ template: './public/index.html', }), ], output: { publicPath: 'http://localhost:3001/', }, };- Test
products-app: Runcd products-app && npm start. You should see the “Products Available” list onhttp://localhost:3001.
Phase 3: Integrate Product List into Host
- Update
host-app/webpack.config.js: AddproductsApptoremotes.// host-app/webpack.config.js // ... plugins: [ new ModuleFederationPlugin({ name: 'hostApp', remotes: { productsApp: 'productsApp@http://localhost:3001/remoteEntry.js', // Add productsApp }, // ... }), // ... ], // ... - Update
host-app/src/index.tsx: Dynamically import and renderProductList.// host-app/src/index.tsx import React, { Suspense, lazy } from 'react'; import ReactDOM from 'react-dom/client'; const RemoteProductList = lazy(() => import('productsApp/ProductList')); const App = () => { return ( <div> <nav style={{ padding: '10px', backgroundColor: '#333', color: 'white', display: 'flex', justifyContent: 'space-between' }}> <h1 style={{ margin: 0 }}>E-commerce Host</h1> <div> <a href="#" style={{ color: 'white', marginRight: '15px' }}>Home</a> <a href="#" style={{ color: 'white' }}>Cart (0)</a> </div> </nav> <div style={{ padding: '20px' }}> <h2>Welcome to our Store!</h2> <Suspense fallback={<div>Loading Products...</div>}> <RemoteProductList /> </Suspense> </div> </div> ); }; const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement); root.render(<App />); - Run and Verify:
- Open
Terminal 1:cd packages/products-app && npm start - Open
Terminal 2:cd packages/host-app && npm start - Navigate to
http://localhost:3000. You should now see theProductListmicro-frontend rendered within thehost-app!
- Open
Phase 4: Create Shopping Cart Micro-Frontend (cart-app) and Inter-Micro-Frontend Communication
This phase will introduce a cart-app and demonstrate how micro-frontends can communicate.
Create
cart-app:cd module-federation-tutorial/packages mkdir cart-app cd cart-app npm init -y npm install react react-dom webpack webpack-cli webpack-dev-server html-webpack-plugin babel-loader @babel/core @babel/preset-react --save-dev npm install --save-dev typescript @types/react @types/react-dom @types/webpack @types/webpack-dev-servercart-app/public/index.html: Similar toproducts-app.cart-app/src/Cart.tsx: This component will manage cart state. It will expose a way for other apps to “add to cart” and will also expose its display component.// cart-app/src/Cart.tsx import React, { useState, useEffect } from 'react'; interface CartItem { id: number; name: string; price: number; quantity: number; } // A simple global event emitter for cross-microfrontend communication const eventEmitter = new EventTarget(); export const addToCart = (product: { id: number; name: string; price: number }) => { const event = new CustomEvent('add-to-cart', { detail: product }); eventEmitter.dispatchEvent(event); }; export const CartDisplay: React.FC = () => { const [cartItems, setCartItems] = useState<CartItem[]>([]); useEffect(() => { const handleAddToCart = (event: Event) => { const product = (event as CustomEvent).detail; setCartItems(prevItems => { const existingItem = prevItems.find(item => item.id === product.id); if (existingItem) { return prevItems.map(item => item.id === product.id ? { ...item, quantity: item.quantity + 1 } : item ); } return [...prevItems, { ...product, quantity: 1 }]; }); }; eventEmitter.addEventListener('add-to-cart', handleAddToCart as EventListener); return () => { eventEmitter.removeEventListener('add-to-cart', handleAddToCart as EventListener); }; }, []); const totalItems = cartItems.reduce((sum, item) => sum + item.quantity, 0); const totalPrice = cartItems.reduce((sum, item) => sum + (item.price * item.quantity), 0); // This could also update a global state or emit an event for the host to update its cart count useEffect(() => { const cartUpdateEvent = new CustomEvent('cart-updated', { detail: { totalItems } }); window.dispatchEvent(cartUpdateEvent); }, [totalItems]); return ( <div style={{ border: '1px solid #ccc', padding: '15px', borderRadius: '8px', backgroundColor: '#f9f9f9', marginTop: '20px' }}> <h3>Your Shopping Cart ({totalItems} items)</h3> {cartItems.length === 0 ? ( <p>Cart is empty.</p> ) : ( <ul style={{ listStyle: 'none', padding: 0 }}> {cartItems.map(item => ( <li key={item.id} style={{ marginBottom: '5px' }}> {item.name} x {item.quantity} - ${item.price * item.quantity} </li> ))} </ul> )} <p style={{ fontWeight: 'bold' }}>Total: ${totalPrice.toFixed(2)}</p> <button style={{ padding: '10px 15px', backgroundColor: 'purple', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer' }}> Checkout </button> </div> ); }; // We export both for different consumption patterns export default CartDisplay;- Encourage Independent Problem Solving: How would you modify
products-app’s “Add to Cart” button to call theaddToCartfunction exposed bycart-app? Think about how you’dimportit.
- Encourage Independent Problem Solving: How would you modify
cart-app/src/index.tsx:// cart-app/src/index.tsx import React from 'react'; import ReactDOM from 'react-dom/client'; import { CartDisplay } from './Cart'; const App = () => { return ( <div> <CartDisplay /> </div> ); }; const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement); root.render(<App />);cart-app/webpack.config.js: Configure it as a remote, exposingCartDisplayandaddToCarton port3002.// cart-app/webpack.config.js const HtmlWebpackPlugin = require('html-webpack-plugin'); const { ModuleFederationPlugin } = require('webpack').container; const path = require('path'); module.exports = { entry: './src/index.tsx', mode: 'development', devServer: { port: 3002, historyApiFallback: true, }, resolve: { extensions: ['.tsx', '.ts', '.js'], }, module: { rules: [ { test: /\.(js|jsx|ts|tsx)$/, exclude: /node_modules/, use: { loader: 'babel-loader', options: { presets: ['@babel/preset-react', '@babel/preset-typescript'], }, }, }, ], }, plugins: [ new ModuleFederationPlugin({ name: 'cartApp', filename: 'remoteEntry.js', exposes: { './CartDisplay': './src/Cart', // Expose the CartDisplay component './addToCart': './src/Cart', // Expose the addToCart function }, shared: { react: { singleton: true, requiredVersion: '^18.2.0' }, 'react-dom': { singleton: true, requiredVersion: '^18.2.0' }, }, }), new HtmlWebpackPlugin({ template: './public/index.html', }), ], output: { publicPath: 'http://localhost:3002/', }, };Update
host-app/webpack.config.js: AddcartApptoremotes.// host-app/webpack.config.js // ... plugins: [ new ModuleFederationPlugin({ name: 'hostApp', remotes: { productsApp: 'productsApp@http://localhost:3001/remoteEntry.js', cartApp: 'cartApp@http://localhost:3002/remoteEntry.js', // Add cartApp }, // ... }), // ... ], // ...Update
host-app/src/index.tsx: Dynamically import and renderCartDisplayand update the cart count in the nav.// host-app/src/index.tsx import React, { Suspense, lazy, useState, useEffect } from 'react'; import ReactDOM from 'react-dom/client'; const RemoteProductList = lazy(() => import('productsApp/ProductList')); const RemoteCartDisplay = lazy(() => import('cartApp/CartDisplay')); const App = () => { const [cartCount, setCartCount] = useState(0); useEffect(() => { const handleCartUpdate = (event: Event) => { const { totalItems } = (event as CustomEvent).detail; setCartCount(totalItems); }; window.addEventListener('cart-updated', handleCartUpdate as EventListener); return () => { window.removeEventListener('cart-updated', handleCartUpdate as EventListener); }; }, []); return ( <div> <nav style={{ padding: '10px', backgroundColor: '#333', color: 'white', display: 'flex', justifyContent: 'space-between' }}> <h1 style={{ margin: 0 }}>E-commerce Host</h1> <div> <a href="#" style={{ color: 'white', marginRight: '15px' }}>Home</a> <a href="#" style={{ color: 'white' }}>Cart ({cartCount})</a> </div> </nav> <div style={{ padding: '20px' }}> <h2>Welcome to our Store!</h2> <Suspense fallback={<div>Loading Products...</div>}> <RemoteProductList /> </Suspense> <Suspense fallback={<div>Loading Cart...</div>}> <RemoteCartDisplay /> </Suspense> </div> </div> ); }; const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement); root.render(<App />);Update
products-app/src/ProductList.tsxto useaddToCart:First, you’ll need to define
addToCarttype globally or in a declaration file inproducts-appif you are using TypeScript, since it’s a runtime import. For simplicity in this example, we’ll castimport():// products-app/src/ProductList.tsx import React from 'react'; interface Product { id: number; name: string; price: number; } const products: Product[] = [ { id: 1, name: 'Laptop', price: 1200 }, { id: 2, name: 'Mouse', price: 25 }, { id: 3, name: 'Keyboard', price: 75 }, ]; // Dynamically import addToCart function const handleAddToCartClick = async (product: Product) => { try { const { addToCart } = await (import('cartApp/addToCart') as any); // Cast to any for dynamic import addToCart(product); alert(`${product.name} added to cart!`); } catch (error) { console.error('Failed to add to cart:', error); alert('Could not add item to cart. Cart app might not be available.'); } }; const ProductList: React.FC = () => { return ( <div style={{ border: '1px solid #eee', padding: '20px', borderRadius: '8px', marginBottom: '20px' }}> <h2>Products Available</h2> <ul style={{ listStyle: 'none', padding: 0 }}> {products.map(product => ( <li key={product.id} style={{ marginBottom: '10px', padding: '10px', border: '1px solid #ddd', borderRadius: '5px', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}> <span>{product.name} - ${product.price}</span> <button style={{ padding: '8px 12px', backgroundColor: 'green', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer' }} onClick={() => handleAddToCartClick(product)} // Call the remote function > Add to Cart </button> </li> ))} </ul> </div> ); }; export default ProductList;- Update
products-app/webpack.config.js: AddcartAppto itsremotesconfiguration.// products-app/webpack.config.js // ... plugins: [ new ModuleFederationPlugin({ name: 'productsApp', filename: 'remoteEntry.js', exposes: { './ProductList': './src/ProductList', }, remotes: { // products-app needs to consume from cartApp cartApp: 'cartApp@http://localhost:3002/remoteEntry.js', }, shared: { /* ... */ }, }), // ... ], // ...
- Update
Run All Applications:
- Open
Terminal 1:cd packages/products-app && npm start - Open
Terminal 2:cd packages/cart-app && npm start - Open
Terminal 3:cd packages/host-app && npm start - Navigate to
http://localhost:3000. Now, click “Add to Cart” buttons. TheCart Displayshould update, and the cart count in theHostnavigation should also reflect the changes. This demonstrates communication between a Host and two Remotes, and also between two Remotes (Product App calling Cart App’saddToCartfunction).
- Open
Project 2: A Dashboard with Pluggable Widgets (Advanced)
Objective: Create a dashboard application where users can dynamically add and arrange “widgets” (micro-frontends) from a library of available remotes. This project will heavily utilize dynamic remote loading and potentially explore state management across micro-frontends.
Technologies: React, Webpack 5, Module Federation, potentially a simple state management solution (e.g., React Context or a global event bus).
Project Structure:
module-federation-tutorial/
├── packages/
│ ├── dashboard-shell/ (Host application, port 4000)
│ ├── widget-chart-app/ (Remote: Exposes a chart widget, port 4001)
│ ├── widget-table-app/ (Remote: Exposes a data table widget, port 4002)
│ └── widget-text-app/ (Remote: Exposes a simple text display widget, port 4003)
├── package.json
Step-by-Step Guide (High-Level - Focus on Architecture and Dynamic Loading):
Phase 1: Setup Dashboard Shell (dashboard-shell)
- Create
dashboard-shell: Similar setup tohost-appin Project 1, but onport: 4000. dashboard-shell/src/index.tsx: This will be more complex, managing a list of active widgets and dynamically rendering them.- It will have a state
activeWidgets: RemoteWidgetConfig[], whereRemoteWidgetConfigmight includeid,name,remoteName,exposedModule. - It will display buttons or a dropdown to “Add Widget”.
- It will iterate over
activeWidgetsand dynamically import/render each one.
- It will have a state
dashboard-shell/webpack.config.js:name: 'dashboardShell'remotes: {}(empty initially, as we’ll load dynamically)shared: { react: ..., 'react-dom': ... }
Phase 2: Create Widget Micro-Frontends (widget-chart-app, widget-table-app, widget-text-app)
Create each widget app: Follow the
remote-app-1setup pattern for each.widget-chart-apponport: 4001:- Exposes
./ChartWidget(a simple placeholder React component). name: 'chartWidgetApp'
- Exposes
widget-table-apponport: 4002:- Exposes
./TableWidget(a simple placeholder React component). name: 'tableWidgetApp'
- Exposes
widget-text-apponport: 4003:- Exposes
./TextWidget(a simple placeholder React component). name: 'textWidgetApp'
- Exposes
Ensure each widget’s
index.tsxsimply renders its main component, and itswebpack.config.jsexposes that component.
Phase 3: Implement Dynamic Widget Loading in dashboard-shell
Refine
dashboard-shell/src/index.tsx:- Create a function
loadWidget(remoteName: string, exposedModule: string)that dynamically imports the widget using the pattern from Section 3.1 (“Asynchronous Remote Loading (Runtime)”). - Maintain a list of available widgets (e.g.,
{ name: 'Chart', remote: 'chartWidgetApp', module: './ChartWidget', url: 'http://localhost:4001/remoteEntry.js' }). - Implement an
onClickhandler for “Add Widget” that callsloadWidgetand adds the dynamically loaded component todashboard-shell’s state.
// dashboard-shell/src/index.tsx (simplified excerpt) import React, { Suspense, lazy, useState } from 'react'; import ReactDOM from 'react-dom/client'; // Type declarations for webpack globals declare const __webpack_init_sharing__: (shareScope: string) => Promise<void>; declare const __webpack_share_scopes__: any; interface WidgetConfig { id: string; // Unique ID for dashboard layout title: string; remoteName: string; // e.g., 'chartWidgetApp' exposedModule: string; // e.g., './ChartWidget' remoteUrl: string; // e.g., 'http://localhost:4001/remoteEntry.js' } const availableWidgets: Omit<WidgetConfig, 'id'>[] = [ { title: 'Chart Widget', remoteName: 'chartWidgetApp', exposedModule: './ChartWidget', remoteUrl: 'http://localhost:4001/remoteEntry.js' }, { title: 'Table Widget', remoteName: 'tableWidgetApp', exposedModule: './TableWidget', remoteUrl: 'http://localhost:4002/remoteEntry.js' }, { title: 'Text Widget', remoteName: 'textWidgetApp', exposedModule: './TextWidget', remoteUrl: 'http://localhost:4003/remoteEntry.js' }, ]; const loadRemoteWidget = async (config: Omit<WidgetConfig, 'id'>) => { // Basic check if remote already initialized (more robust version needed for production) if (!window[config.remoteName]) { await __webpack_init_sharing__('default'); const script = document.createElement('script'); script.src = config.remoteUrl; script.onerror = (e) => console.error(`Error loading remote ${config.remoteName}:`, e); await new Promise((resolve) => { script.onload = resolve; document.head.appendChild(script); }); // @ts-ignore await window[config.remoteName].init(__webpack_share_scopes__.default); } // @ts-ignore const moduleFactory = await window[config.remoteName].get(config.exposedModule); return moduleFactory(); }; const App = () => { const [renderedWidgets, setRenderedWidgets] = useState<WidgetConfig[]>([]); const addWidget = async (widgetToAdd: Omit<WidgetConfig, 'id'>) => { const id = `${widgetToAdd.remoteName}-${Date.now()}`; // Unique ID for key prop setRenderedWidgets(prev => [...prev, { ...widgetToAdd, id }]); }; return ( <div style={{ fontFamily: 'Arial, sans-serif' }}> <nav style={{ padding: '15px', backgroundColor: '#444', color: 'white' }}> <h1>Dynamic Dashboard</h1> </nav> <div style={{ padding: '20px', borderBottom: '1px solid #ddd' }}> <h3>Available Widgets:</h3> <div style={{ display: 'flex', gap: '10px' }}> {availableWidgets.map((widget, index) => ( <button key={index} onClick={() => addWidget(widget)} style={{ padding: '10px 15px', backgroundColor: '#007bff', color: 'white', border: 'none', borderRadius: '5px', cursor: 'pointer' }} > Add {widget.title} </button> ))} </div> </div> <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))', gap: '20px', padding: '20px' }}> {renderedWidgets.map(widgetConfig => { const RemoteWidget = lazy(() => loadRemoteWidget(widgetConfig).then((mod: any) => ({ default: mod.default || mod }))); return ( <div key={widgetConfig.id} style={{ border: '1px solid #aaa', padding: '15px', borderRadius: '8px', boxShadow: '2px 2px 5px rgba(0,0,0,0.1)' }}> <h4>{widgetConfig.title}</h4> <Suspense fallback={<div>Loading {widgetConfig.title}...</div>}> <RemoteWidget /> </Suspense> </div> ); })} </div> </div> ); }; const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement); root.render(<App />);- Create a function
Run All Applications: Start all widget applications and then the
dashboard-shell. Observe how you can dynamically add widgets to the dashboard at runtime.
Further Exploration for Project 2:
- Widget Configuration: Pass specific props to widgets to customize their data or appearance.
- Drag-and-Drop Layout: Implement a drag-and-drop feature to rearrange widgets on the dashboard.
- Persistent Layout: Save the user’s dashboard layout (which widgets are present and their order) to
localStorageor a backend. - Inter-Widget Communication: Allow widgets to communicate with each other (e.g., a “Filter” widget affecting a “Chart” widget). A global event bus or a shared state management library (exposed as a federated module) could be used.
- Error Handling: Implement robust error boundaries for each dynamically loaded widget.
6. Bonus Section: Further Learning and Resources
Congratulations on making it this far! Module Federation is a powerful tool, and continuous learning is key. Here are some excellent resources to deepen your understanding:
Recommended Online Courses/Tutorials
- Module Federation Official Documentation: While this guide covers the basics, the official documentation is the ultimate source for in-depth information and specific configurations.
- https://webpack.js.org/concepts/module-federation/ (Webpack 5 Official Docs)
- https://module-federation.io/ (Module Federation 2.0 Docs - focuses on the standalone runtime)
- YouTube Tutorials: Many creators offer excellent visual guides. Search for “Module Federation tutorial 2025” or “Micro Frontends Webpack 5”.
- Module Federation Complete Tutorial 2025 Micro Frontend (A good starting point)
Official Documentation
- Webpack Module Federation: The primary source for Webpack’s implementation of Module Federation.
- Module Federation 2.0 (Enhanced Runtime): The new independent documentation site for the latest version.
Blogs and Articles
- Nx Blog: Nx (a monorepo tool) has excellent articles on integrating Module Federation, especially with Rspack.
- https://nx.dev/blog/improved-module-federation (New and Improved Module Federation Experience with Nx)
- Dev.to Articles: A great platform for community-driven articles and practical examples.
- Medium Articles: Another rich source of detailed guides and best practices.
- GitHub Discussions: Often a good place to find cutting-edge information and community insights.
YouTube Channels
- Look for channels specializing in modern JavaScript, React, Webpack, or Micro-Frontends. Some popular ones include:
- Academind
- Traversy Media
- The Net Ninja
- Fireship
- And many independent content creators who deep dive into specific topics.
Community Forums/Groups
- Stack Overflow: For specific technical questions and troubleshooting.
- GitHub Repositories: The official Module Federation and Webpack repositories often have active discussions.
- https://github.com/module-federation/module-federation-examples (Official examples)
- https://github.com/nrwl/nx (Nx Monorepo, excellent MF support)
- Discord Servers: Many large JavaScript and Webpack communities have active Discord servers where you can ask questions and engage with other developers.
Next Steps/Advanced Topics
After mastering the content in this document, consider exploring:
- State Management in Micro-Frontends: Dive deeper into patterns for sharing and managing state across federated applications (e.g., custom event bus, shared Redux store as a federated module, context providers).
- Routing in Micro-Frontends: How to achieve seamless navigation across different micro-frontends (e.g., using
react-router-domwith careful configuration, specialized micro-frontend routers). - Server-Side Rendering (SSR) with Module Federation: This adds significant complexity but is crucial for SEO and initial page load performance in some applications.
- Module Federation with other Build Tools: Explore integration with Vite (@module-federation/vite) or Rspack (built-in support).
- Universal Module Federation (UMF): Explore solutions that allow sharing modules between different JavaScript runtimes (browser, Node.js).
- Testing Micro-Frontends: Strategies for unit, integration, and end-to-end testing in a federated architecture.
- Deployment and CI/CD for Micro-Frontends: Setting up independent pipelines for each micro-frontend, managing releases, and versioning.
- Security Considerations: Best practices for securing federated applications.
Module Federation is a powerful and evolving technology. By diligently working through these concepts and projects, you’ll be well-equipped to tackle the challenges of building large-scale, distributed web applications. Happy coding!
For more advance stuff follow here -> Advanced Micro-Frontends with Module Federation: Mastering Scalability and Complexity (2025 Edition)