How to Build Micro-Frontends Using Webpack Module Federation (with Full Code Example)
π§ What is Micro-Frontend?
Micro-Frontend architecture breaks a frontend app into independent, deployable features owned by separate teams β much like microservices in the backend.
Instead of a monolithic React or Angular app, you build small frontends (e.g., dashboard, login, profile) and compose them together.
βοΈ Why Use Webpack Module Federation?
Webpack 5 introduces Module Federation, which allows an app to dynamically load code from another app at runtime.
This makes it possible to build fully independent micro frontends that:
- Can be deployed separately
- Share common dependencies
- Work together seamlessly
π¦ Folder Structure
We’ll build:
host-appβ Shell app that loads remote modulesremote-appβ A micro frontend exposing a component
microfrontend-example/
βββ host-app/
βββ remote-app/
1οΈβ£ Set Up the Remote App
This is your micro frontend that exposes a component (Hello) to other apps.
π§ remote-app/package.json
{
"name": "remote-app",
"version": "1.0.0",
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"webpack": "^5.0.0",
"webpack-cli": "^4.0.0",
"webpack-dev-server": "^4.0.0",
"html-webpack-plugin": "^5.5.0"
}
}
π Why?
This sets up a standalone React project that uses Webpack 5 and React. This app will expose a component (Hello) using Webpackβs Module Federation.
Install dependencies:
cd remote-app
npm install
π remote-app/src/Hello.js
import React from "react";
const Hello = () => {
return <h2>Hello from Remote App π</h2>;
};
export default Hello;
π Why?
This is the component that we will expose to the host. Itβs a simple React component that renders a message.
π remote-app/src/index.js
import React from "react";
import ReactDOM from "react-dom";
import Hello from "./Hello";
const App = () => <Hello />;
ReactDOM.render(<App />, document.getElementById("root"));
π Why?
This is the entry point for remote-app, which directly renders the Hello component into the DOM when run directly (e.g., for testing).
π οΈ remote-app/webpack.config.js
const HtmlWebpackPlugin = require("html-webpack-plugin");
const ModuleFederationPlugin = require("webpack").container.ModuleFederationPlugin;
module.exports = {
entry: "./src/index.js",
mode: "development",
devServer: {
port: 3001,
},
output: {
publicPath: "auto",
},
plugins: [
new ModuleFederationPlugin({
name: "remoteApp",
filename: "remoteEntry.js",
exposes: {
"./Hello": "./src/Hello",
},
shared: { react: { singleton: true }, "react-dom": { singleton: true } },
}),
new HtmlWebpackPlugin({
template: "./public/index.html",
}),
],
};
π Why?
This is the heart of Module Federation.
name: Unique name for the remote app.filename: This file (remoteEntry.js) is what other apps will load.exposes: This tells Webpack which module we want to share (here,./Hello).shared: Ensures both host and remote share a single instance of React.
π remote-app/public/index.html
<!DOCTYPE html>
<html>
<body>
<div id="root"></div>
</body>
</html>
π Why?
Standard HTML container for rendering the React app.
Start the remote app:
npm start
π Why?
Runs a dev server on http://localhost:3001. This will host the remoteEntry.js file and serve the app for direct viewing and testing.
2οΈβ£ Set Up the Host App
This is your container application β it dynamically loads remote components and renders them.
π§ host-app/package.json
Same as remote app β with dependencies like React, Webpack, etc.
Install everything:
cd host-app
npm install
π host-app/src/App.js
import React from "react";
// Dynamically load the remote component
const RemoteHello = React.lazy(() => import("remoteApp/Hello"));
const App = () => (
<React.Suspense fallback={<div>Loading...</div>}>
<RemoteHello />
</React.Suspense>
);
export default App;
π Why?
We use React.lazy() to dynamically import the Hello component from the remote app at runtime.
This shows a fallback while the remote component is loading. Reactβs Suspense is needed when using React.lazy.
π host-app/src/index.js
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
ReactDOM.render(<App />, document.getElementById("root"));
π Why?
Standard React entry point. Mounts the app to the HTML div.
π οΈ host-app/webpack.config.js
const HtmlWebpackPlugin = require("html-webpack-plugin");
const ModuleFederationPlugin = require("webpack").container.ModuleFederationPlugin;
module.exports = {
entry: "./src/index.js",
mode: "development",
devServer: {
port: 3000,
},
output: {
publicPath: "auto",
},
plugins: [
new ModuleFederationPlugin({
name: "hostApp",
remotes: {
remoteApp: "remoteApp@http://localhost:3001/remoteEntry.js",
},
shared: { react: { singleton: true }, "react-dom": { singleton: true } },
}),
new HtmlWebpackPlugin({
template: "./public/index.html",
}),
],
};
π Why?
remotes: This tells the host app where to load modules from.remoteApp@http://localhost:3001/remoteEntry.js: This matches whatremote-appexposed β we can now loadremoteApp/Hello.
π host-app/public/index.html
<!DOCTYPE html>
<html>
<body>
<div id="root"></div>
</body>
</html>
π Why?
Root container where React app will render in the browser.
Start the host app:
npm start
π Why?
Starts the host app at http://localhost:3000, which will now load and render the Hello component from the remote app.
β Final Behavior
When you open the browser to http://localhost:3000, this happens:
It renders the component inside the host UI.
host-app loads.
It reads remoteEntry.js from http://localhost:3001.
It loads the Hello component from the remote.
β You should see:
Hello from Remote App π
π§ Summary of Each Step
| Step | Action | Purpose |
|---|---|---|
| 1 | Create remote-app | A micro-frontend with an exposed component |
| 2 | Use ModuleFederationPlugin | Make component available to others |
| 3 | Start remote-app | Serve the remoteEntry.js file |
| 4 | Create host-app | Container app to load remote content |
| 5 | Configure remotes | Link host to remote |
| 6 | Use React.lazy | Dynamically load remote component |
| 7 | Start host-app | View full system in action |
