The Micro Frontend style of architecture design does for the frontend of an application what microservices do for the backend, breaking monolithic structures into smaller components that can then be assembled on a single page.
Module Federation allows a JavaScript application to dynamically load code from another application and in the process, share dependencies. If an application consuming a federated module does not have a dependency needed by the federated code, Webpack will download the missing dependency from that federated build origin.
Let’s start with setting up the environment. I’m using
Run the following npx command on a terminal to install and bootstrap the application using “create-mf-app”. Let’s call our application “container”
npx create-mf-app
After completing the first step, The directory initially looks something like this:
I will not deep dive into the folder structure, It’s similar to the create-react-app folder structure.
import('./App');
Let’s quickly create another app called Counter using the same steps as above.
npx create-mf-app
Inside the counter app, I have created a Counter component in the components folder.
**src/components/Counter.jsx
import React, { useState } from "react";
export const Counter = () => {
const [count, setCount] = useState(0);
const onIncrement = () => setCount(count + 1);
const onDecrement = () => setCount(count - 1);
return (
<div>
<h1>Counter App</h1>
<p>Current count: <strong>{count}</strong></p>
<button onClick={onIncrement}>+</button>
<button onClick={onDecrement}>-</button>
</div>
);
}
Let’s update the webpack.config.js file inside the Counter app. Add ModuleFederationPlugin to the plugins array with the following configuration:
**webpack.config.js
const HtmlWebPackPlugin = require("html-webpack-plugin");
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
const deps = require("./package.json").dependencies;
module.exports = {
output: {
publicPath: "http://localhost:8081/",
},
resolve: {
extensions: [".tsx", ".ts", ".jsx", ".js", ".json"],
},
devServer: {
port: 8081,
historyApiFallback: true,
},
module: {
rules: [
{
test: /\.m?js/,
type: "javascript/auto",
resolve: {
fullySpecified: false,
},
},
{
test: /\.(css|s[ac]ss)$/i,
use: ["style-loader", "css-loader", "postcss-loader"],
},
{
test: /\.(ts|tsx|js|jsx)$/,
exclude: /node_modules/,
use: {
loader: "babel-loader",
},
},
],
},
plugins: [ // This is important part
new ModuleFederationPlugin({
name: "counter",
filename: "remoteEntry.js",
remotes: {},
exposes: {
"./Counter": "./src/components/Counter",
},
shared: {
...deps,
react: {
singleton: true,
requiredVersion: deps.react,
},
"react-dom": {
singleton: true,
requiredVersion: deps["react-dom"],
},
},
}),
new HtmlWebPackPlugin({
template: "./src/index.html",
}),
],
};
Let’s update the webpack.config.js file inside the Container app.
**webpack.config.js
const HtmlWebPackPlugin = require("html-webpack-plugin");
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
const deps = require("./package.json").dependencies;
module.exports = {
output: {
publicPath: "http://localhost:8080/",
},
resolve: {
extensions: [".tsx", ".ts", ".jsx", ".js", ".json"],
},
devServer: {
port: 8080,
historyApiFallback: true,
},
module: {
rules: [
{
test: /\.m?js/,
type: "javascript/auto",
resolve: {
fullySpecified: false,
},
},
{
test: /\.(css|s[ac]ss)$/i,
use: ["style-loader", "css-loader", "postcss-loader"],
},
{
test: /\.(ts|tsx|js|jsx)$/,
exclude: /node_modules/,
use: {
loader: "babel-loader",
},
},
],
},
plugins: [ // This is important part
new ModuleFederationPlugin({
name: "container",
filename: "remoteEntry.js",
remotes: {
counter: "counter@http://localhost:8081/remoteEntry.js",
},
exposes: {},
shared: {
...deps,
react: {
singleton: true,
requiredVersion: deps.react,
},
"react-dom": {
singleton: true,
requiredVersion: deps["react-dom"],
},
},
}),
new HtmlWebPackPlugin({
template: "./src/index.html",
}),
],
};
NOTE: The remote objects will have to define all the entry points exposed from remote apps, remotes entry has the following structure:
{ "app-name": "name@<remote-host>/remoteEntry.js" }
**src/App.jsx
import React from "react";
import ReactDOM from "react-dom";
import { Counter } from 'counter/Counter';
import "./index.css";
const App = () => (
<div className="container">
<h1>Container App</h1>
<Counter /> // Micro frontend app
</div>
);
ReactDOM.render(<App />, document.getElementById("app"));
GitHub Repo: https://github.com/devsmitra/micro
Reference:
Thank you for reading 😊
Got any questions or additional? please leave a comment.
Also published here.
Javascript: No More callbacks, Use Promisify to Convert Callback to Promise
Creating a Custom Hook for Fetching Asynchronous Data: useAsync Hook with Cache
Javascript Promise Methods with polyfill example: A Cheat Sheet for Developer