Micro Front-ends (Part 1)

“Micro Frontend” is becoming more popular because they make it possible to create contemporary web applications with fewer resources. They are modular, decoupled, and scalable. It focuses on small parts that can be used repeatedly throughout your program.

In this article, we’ll take a look at how micro frontends work and why they’re so beneficial. We will also write some code to build a simple micro-frontends application using ReactJS, VueJS and Module federation (using Webpack), so you can learn more about micro frontends.

Prolog: What and Why Micro Front-ends?

https://microfrontends.com/

Micro front-end is an architecture for building modern web applications that are composed of multiple independent parts or “micro apps” each with its own codebase, technology stack, team members and development process. This approach breaks up large monolithic applications into smaller pieces which makes them easier to build, maintain and scale independently from one another while still providing seamless integration between them at runtime on both desktop as well as mobile devices.

Individual teams may now concentrate on their unique areas without having to coordinate with other teams working on other application components, which results in speedier development cycles overall because changes don’t need to be approved by everyone else before being implemented live. Each micro app has its own resources, so if one part needs more power than others, those resources can be allocated accordingly without affecting performance elsewhere in the system. This leads to better performance throughout the system. In addition to being able to deploy new features more quickly, this improved scalability also results in better performance overall.

If you have any problem, take a look at our sample code on Github

I. Folder structure & Preview

Before we started, this is the folder structure we will be created

micro-fes/
├─ container/
│  ├─ App.js
├─ counter/
│  ├─ Counter.js
├─ message/
│  ├─ Message.js

II. The Counter – ReactJS Application

1. Installation

Let start by create a new ReactJS app with CRA.

npx create-react-app counter

We will need webpack so let install it as well

yarn add webpack

2. It’s coding time

2.1. Update start script for the application

Change package.json start script in order for it to compile through our webpack config

"scripts": {
    "start": "webpack serve --open --mode development", // <-- Change this
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  }

2.2. Create a Counter component

import React from "react";

const Counter = ({ counter, setCounter }) => {
  return (
    <div style={{ border: '1px solid blue', borderRadius: 5, padding: 10 }}>
      <div style={{ color: "blue" }}>Counter: ReactJS</div>
      <div style={{ display: "flex", gap: 50, alignItems: "center", justifyContent: "space-around" }}>
        <button onClick={() => setCounter(counter - 1)}>Decrease</button>
        <p>{counter}</p>
        <button onClick={() => setCounter(counter + 1)}>Increase</button>
      </div>
    </div>
  );
};

export default Counter;

Notice that the component receives counter and setCounter as props, this is my intention to show you one of many ways front-ends in micro-frontends can talk to each other.

2.3 Create config for Webpack

// 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",
          options: {
            presets: ["@babel/preset-react"]
          }
        },
      },
			{
        test: /\\.(jpe?g|png|gif|svg)$/i,
        type: "asset/resource"
			}
    ],
  },
  plugins: [
    new ModuleFederationPlugin({
      name: "counter",
      filename: "remoteEntry.js",
      remotes: {},
      exposes: {
        "./Counter": "./src/Counter",
      },
      shared: {
        ...deps,
        react: {
          singleton: true,
          requiredVersion: deps.react,
        },
        "react-dom": {
          singleton: true,
          requiredVersion: deps["react-dom"],
        },
      },
    }),
    new HtmlWebPackPlugin({
      template: "./public/index.html",
    }),
  ],
};

Let’s explain more about ModuleFederationPlugin. This is the heart of our micro-fes demo.

  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.

So in this app, we can translate the above config to:

“Let’s expose the Counter component through a remote name counter by access the http://localhost:8081/remoteEntry.js, and while doing so, shared the dependencies between counter and whatever uses the counter.”

2.4. Modify entry point of the application

Replace content inside of index.js file.

// index.js
import("./App"); // 👈 Add this line
// 👇🏻 Remove the default file content
import React from 'react';
...

2.5. Update App Component (optional)

After modify the index.js, we can no longer see the app by access localhost:8081. In order to have the app running again, we just simply move the render part in to the App Component.

// App.js
// 👇🏻 Add this to the end of the file
const root = ReactDOM.createRoot(document.getElementById('root'));
  root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

2.6. Start the app

Start the app

yarn start 
// or `npm run start`

If you run into an error like

URIError: Failed to decode param '/%PUBLIC_URL%/manifest.json'

To resolve this issue, we need to update the path to asset files inside public/index.html

Change

%PUBLIC_URL%/favicon.ico

into

./favicon.ico

Restart the app, the error should go away.

🎉 Nice, I down, two more to go.

Take a sip, we are almost there!

III. The Message – Vue Application

1. Installation

npm init vue@latest

The CLI will ask us some question to initialize the Vue application, we will ignore all of them for now.

✔ Project name: message
✔ Add TypeScript? No
✔ Add JSX Support? No
✔ Add Vue Router for Single Page Application development? No
✔ Add Pinia for state management? No
✔ Add Vitest for Unit testing? No
✔ Add Cypress for both Unit and End-to-End testing? No
✔ Add ESLint for code quality? No
✔ Add Prettier for code formatting? No

Vue 3 comes with Vite (a Webpack alternative) out of the box. But in this blog, we will ignore this and use Webpack instead. So let add webpack to our Vue:

yarn add -D webpack

// Since we switch to Webpack, there are a few more dependencies we are going to need
yarn add -D babel-loader @babel/core @babel/plugin-transform-runtime @babel/preset-env css-loader pos
tcss-loader style-loader html-webpack-plugin vue-loader

2. It’s coding time

2.1. Update start script to compile with our webpack

// package.json
"scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview",
    "start": "webpack serve --mode development"
  },

2.2. Create Webpack config file

// webpack.config.js
const { VueLoaderPlugin } = require("vue-loader");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
const packageJson = require("./package.json");

module.exports = {
  entry: "./src/main.js",
  output: {
    publicPath: "<http://localhost:8082/>",
  },
  resolve: {
    extensions: [".js", ".vue"],
  },
  devServer: {
    port: 8082,
    historyApiFallback: {
      index: "/index.html",
    },
  },
  module: {
    rules: [
      {
        test: /\\.vue$/,
        use: "vue-loader",
      },
      {
        test: /\\.(css|s[ac]ss)$/i,
        use: ["style-loader", "css-loader", "postcss-loader"],
      },
      {
        test: /\\.m?js$/,
        exclude: /node_modules/,
        use: {
          loader: "babel-loader",
          options: {
            presets: ["@babel/preset-env"],
            plugins: ["@babel/plugin-transform-runtime"],
          },
        },
      },
			{
        test: /\\.(jpe?g|png|gif|svg)$/i,
        type: "asset/resource"
			}
    ],
  },

  plugins: [
    new VueLoaderPlugin(),
    new ModuleFederationPlugin({
      name: "message",
      filename: "remoteEntry.js",
      exposes: {
        "./Message": "./src/bootstrap",
      },
      shared: packageJson.dependencies,
    }),
    new HtmlWebpackPlugin({
      template: "./index.html",
    }),
  ],
};

“Let’s expose the Message component through a remote name message by access the http://localhost:8082/remoteEntry.js, and while doing so, shared the dependencies between counter and whatever uses the counter.”

2.3. Modify the entry point of the application

// src/main.js
import("./bootstrap")

2.4. Bootstrap the application

// src/bootstrap.js
import { createApp } from 'vue'
import App from './App'

const mount = (el, props) => {
    const app = createApp(App, props)
    app.mount(el)
}

if (process.env.NODE_ENV === 'development') {
    const devRoot = document.querySelector('#app')
    if (devRoot) {
        mount(devRoot)
    }
}

export { mount }

Unlike the Counter front-end, Message front-end is a Vue app. Container front-end – a ReactJS application – will not know how to handle the Vue part, so instead of just export the Component, we need to export a function from Vue that is going to handle the entire mounting process for the Vue component.

2.5. Create the Message component

// src/App.vue
<script setup>
import "./assets/base.css";

defineProps({
  msg: {
    type: String,
    required: true
  },
})
</script>

<template>
  <div class="greetings">
    <h3>{{ msg }}</h3>
    <p>
      App3: VueJS
    </p>
  </div>
</template>

<style scoped>
.greetings {
  margin-top: 10px;
  border: 1px solid green;
  border-radius: 5px;
  padding: 10px;
}

p {
  color: green;
}
</style>

At this point, we have 2 applications, 1 ReactJS and 1 Vue, let combine and see them inside one Container.

IV. The Container – ReactJS Application

Since this is also a ReactJS Application, the same Installation process will be the same with the Counter application.

1. Installation

Let start by create a new ReactJS app with CRA.

npx create-react-app container

We will need webpack so let install it as well

yarn add webpack

2. Update code

Change package.json start script in order for it to compile through our webpack config

"scripts": {
    "start": "webpack serve --open --mode development", // <-- Change this
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },

Add a new webpack.config.js file

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",
          options: {
            presets: ["@babel/preset-react"]
          }
        },
      },
			{
        test: /\\.(jpe?g|png|gif|svg)$/i,
        type: "asset/resource"
			}
    ],
  },
  plugins: [ // This is important part
    new ModuleFederationPlugin({
      name: "container",
      filename: "remoteEntry.js",
      remotes: {
        counter: "counter@<http://localhost:8081/remoteEntry.js>",
        message: "message@<http://localhost:8082/remoteEntry.js>"
      },
      exposes: {},
      shared: {
        ...deps,
        react: {
          singleton: true,
          requiredVersion: deps.react,
        },
        "react-dom": {
          singleton: true,
          requiredVersion: deps["react-dom"],
        },
      },
    }),
    new HtmlWebPackPlugin({
      template: "./public/index.html",
    }),
  ],
};

Notice that, this time, we will use the remote inside of ModuleFederationPlugin

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"

For example:

remotes: {
	counter: "counter@<http://localhost:8081/remoteEntry.js>",
	message: "message@<http://localhost:8082/remoteEntry.js>"
}

As you can already guessed, counter and message are our 2 previous applications.

2.3. Use the micro-apps

import React, { useState, useEffect, useRef } from "react";
import ReactDOM from "react-dom/client";

import Counter from "counter/Counter";
import { mount } from "message/Message";

import "./App.css";

function App() {
  const [counter, setCounter] = useState(0);
  const ref = useRef(null);

  useEffect(() => {
    mount(ref.current, { msg: `A message from ReactJS - counter: ${counter}` });
  }, [counter]);

  return (
    <div
      className="App"
      style={{ border: "1px solid orange", borderRadius: 5 }}
    >
      <div style={{ color: "orange" }}>Container: ReactJS</div>
      <header className="App-header">
        <Counter counter={counter} setCounter={setCounter} />
        <div ref={ref} />
      </header>
    </div>
  );
}

export default App;

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

About the Counter component, things are very straight forward, it’s simply just another ReactJS Component.

The interesting thing is the Vue Message component, we will need to import the mount function, pass in a HTMLElement, and (optionally) it’s props. In this example, I try to make things more interesting by putting the mount function inside an useEffect which will be executed every time the counterstate is changed. As a result, our Vue Message component can get the state counter and display it, in sync with the Container app. In short, Message and Container can talk to each other despite being 2 separated applications.

Change the index.js to use App

//index.js
import("App");

The only thing left is start all 3 applications and access the Container via http://localhost:8080.

A Micro Front-end Counter with ReactJS and VueJS

V. Conclusion

Micro front-end is an emerging technology that has the potential to revolutionize how web applications are built. By allowing developers to break down their application into smaller, more manageable components, micro front-end offers a number of benefits such as improved scalability and better team collaboration. This means faster development cycles and greater flexibility for businesses when it comes to responding quickly to customer needs. But we’ve only just scratched the surface of what this technology can do – with new tools being developed all the time, there’s so much more waiting in store for us! With its ability to help teams create simpler yet powerful web apps faster than ever before, micro front-end is sure become one of the most important technologies used by developers in years come.

This is just one of many sections about Micro Frontend, a quick overview and straightforward demonstration to get you interested in this brand-new, high-potential technology. Come back again to learn more about Micro Frontend from the people at DelightInCode, who are eager to share their experience with everyone.

VI. Read more


Are you looking to build a website for your business or hobby? Don’t hesitate to contact us! We are here to help.

At DelightInCode, we understand that building a website can seem like an intimidating task, especially if you’re new to the process. That’s why we offer our expertise and guidance throughout the entire process – from conception through launch. Our team of experienced professionals will work with you every step of the way, ensuring that your vision is realized in beautiful form and function.

So don’t hesitate – get in touch today if there’s anything at all we can do for you! With our help, launching a successful website has never been easier!

Leave A Reply