Micro-Frontends in 2023: The Complete Guide

Written by devsmitra | Published Invalid Date
Tech Story Tags: microfrontend | reactjs | react-native | react | webpack | javascript | web-development | webdev | web-monetization

TLDRThe Micro Frontend style of architecture design does for the frontend of an application what microservices do for the backend. It breaks monolithic structures into smaller components that can then be assembled on a single page. A micro frontend is more modular and reusable and is more scalable.via the TL;DR App

What is a micro frontend?

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.

https://youtu.be/u4Z9INmHhCM?autoplay=1?embedable=true

Pros and cons of micro frontend:

Pros:

  • A micro frontend is more modular and reusable.
  • A micro frontend is more scalable.
  • The micro frontend is more maintainable.
  • Independent and Faster development.
  • Testing separate applications are easy.
  • Different front-end technologies can be used for different projects(like React, Angular, Vue.js, etc).

Cons:

  • Testing the entire application is not easy.
  • Sharing code, state(data), etc is not easy.

Module federation

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.

Prerequisite:

  • Make sure you have installed the latest Node.js
  • Basic knowledge of React.js and Webpack 5+.
  • Code Editor

I'll create 2 apps in this article:

  • First: Container app that will be used as a base for the micro frontends.
  • Second: The counter app that will get rendered inside the container app.

Let’s start with setting up the environment. I’m using create-mf-app to create a react app.

Container App

Installation

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.

NOTE: Only difference here is the index.js file, Which loads the app dynamically.

import('./App');

Let’s quickly create another app called Counter using the same steps as above.

Counter App

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 understand what each option is:

  1. name: Name of the remote app
  2. filename: Entry point(remoteEntry.js) for the counter app.
  3. remotes: Add remotes entry here (relevant for the container)
  4. exposes: All the component names that you want to expose to the container app.
  5. shared: container all the dependencies that you want to share between the container and the counter app.

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: "[email protected]://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": "[email protected]<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"));

Time to run both the apps

Counter App

Container App

GitHub Repo: https://github.com/devsmitra/micro

Reference: https://github.com/jherr/create-mf-app

Thank you for reading 😊

Got any questions or additional? please leave a comment.


Also published here.

Must Read If You Haven't

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



Written by devsmitra | I'm a technology enthusiast who does web development.
Published by HackerNoon on Invalid Date