Front-end Development
10 min read

Micro Frontends, are they the future of web development?

Web applications have advanced a lot over the last decade. They have advanced from a simple static page for your pet, to fully functional tools that allow you to connect with friends, buy books and even author content for the web. To allow for this advancement, the technology behind web applications has had to grow a lot. One of the most recent advancements in how we structure web applications is the concept of micro frontends. In this article we will build a web application and learn about micro frontends along the way.

Chris Laughlin
Published July 11, 2022

Why Micro Frontends

Traditional web applications are usually built as a single page application (SPA). There are many benefits to a SPA, including faster load times between page loads, but a key benefit is separation from the backend/data layer of the application. Having a clearer separation of concerns between frontend and backend logic makes it easier for product teams to make UI changes independent of the API and DB layers. In other words, you can have one team that is focused on delivering a strong API experience, while another team works on delivering the best end user experience.

However as the application grows, a SPA can become hard to maintain and deploy. Similar to the backend trend of microservices, the UI can use micro frontends to achieve the same scalability and speed of delivery. Imagine a UI that has multiple teams pushing features at the same time. Micro frontends can help teams ship faster by separating the UI into separate modules that can be deployed independently of each other. This can help speed up the pace of development while ensuring the application feels like one cohesive product to the end user.

Our Web Application

To test out the full power of micro frontends, we will build a web store application that uses the following:

The application we are going to build is a pizza ordering application that will consist of a container application and two module federated components/packages: basket and product list. The final source code for the project can be found here.

Before we start writing any code, let’s take a look at the concepts mentioned earlier.

Our application structure can be broken down into two main parts, the container (home) and the packages (basket and product list).

Container

The container, or as we have named it the home application, is a standard React web application that uses Webpack to compile and build. You can check out the home folder for how this is configured. In our application entry (package/home/src/index.js) file we will import our React application:

import("./App");

This will be the container for our shop, and lives inside the src/App.jsx file:

import React, { useState } from "react";
import ReactDOM from "react-dom";

import "./index.css";
import "./app.css";
import "./productList.css";

const App = () => {
  const [selected, setSelected] = useState([]);

  const onBuyItem = (item) => {
    setSelected((curr) => [...curr, item]);
  };

  return (
    <div className="app">
      <h1>Pizza Store</h1>
      <div className="app-content">
        <section>{"Product List Goes Here"}</section>
        <section>{"Shopping Basket Goes here "}</section>
      </div>
    </div>
  );
};

ReactDOM.render(<App />, document.getElementById("app"));

As you can see, we don’t have our product list or shopping brackets components yet. However, we can still run this application by running npm start and it will render a very basic application with no functionality.

So let’s step up our directory structure to the packages/product-list component and start building out our first federated module.

Product List

Our product list module is built using Webpack also, but is a bit different than our home container. Let’s look into how it’s built.

Our entry point (packages/product-list/src/index.js) exports our React component instead of importing a React application. This is similar to how we would build a reusable React component inside of a larger application.

import ProductList from "./productList";

export { ProductList };

In the product list index, we first import our product list and then export it as a named export. Let’s take a look into the product list component:

import React from "react";
import Products from "./products.json";

const ProductList = ({ onBuyItem }) => {
  return (
    <ul>
      {Products.map((product) => {
        return (
          <li key={product.id}>
            <span>{product.name}</span>
            <span>$ {product.price}</span>
            <button onClick={() => onBuyItem(product)}>Buy</button>
          </li>
        );
      })}
    </ul>
  );
};

export default ProductList;

This is a very basic React component that takes in an onBuyItem function prop and renders a HTML unordered list using product data that was imported from the same package. Each list item has a “Buy” button that will trigger the prop, passing in the selected product. Before this package can be consumed by our home container, we need to add Webpack module federation to the product list Webpack config. In the Webpack config we need to add a plugin that will allow Webpack to serve this code as a federated module that a corresponding Webpack served application can consume.

plugins: [
   new ModuleFederationPlugin({
     name: 'productList',
     library: {
       type: 'var', name: 'productList'
     },
     filename: 'remoteEntry.js',
     exposes: {
       './ProductList': './src/ProductList'
     },
     shared: require('./package.json').dependencies
   })
 ],

In the module federation plugin options, we need to detail what the module is and how it’s exposed. We’ll define the following options:

Basket

Our basket module is very much the same as the product list module; it’s also a simple React component that takes in props. The two things we do differently compared to the product list is that (1) we are using styled-components to add CSS styles to the component (2) we are using Ramda to perform logic on the data passed to the component.

import React from "react";
import styled from "styled-components";
import { reduce, pluck, add, map } from "ramda";

const StyledBasket = styled.div`
  height: 150px;
  width: 200px;
  border: 10px solid pink;
  border-radius: 10px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
`;

const totalPrice = (items) => {
  return reduce(add, 0, map(parseFloat, pluck("price", items)));
};

const Basket = ({ items, onClear }) => {
  return (
    <StyledBasket>
      <h3>BASKET</h3>
      <span>PRODUCT COUNT: {items.length}</span>
      <span>TOTAL: {Number.parseFloat(totalPrice(items)).toFixed(2)}</span>
      <button onClick={onClear}>CLEAR</button>
    </StyledBasket>
  );
};

export default Basket;

Once we build out the container more we will see how the items and onClear props come into play. The Webpack config and module federation plugin settings are the same except they replace product list with basket.

Container Continued

Let’s jump back to our container and start bringing everything together. First, we need to update the index.html file in our home application. This is the entry point of the React application. By default, Webpack will inject a script tag for our main JS application. We want to add in script tags that will point to our modules. Each module’s folder has a run script that will bundle and host the JS on a local HTTP server. In a real world deployed application, these would point to the where you host the code e.g. AWS Cloudfront/S3

<script src="http://localhost:8081/remoteEntry.js"></script>
<script src="http://localhost:8082/remoteEntry.js"></script>

We’ve added a script tag for both remote entries so that both are served and executed on initial page load. In this example we used Webpack’s default filename, but you can customize the filenames in the Webpack configs.

Next we need to update the home’s Webpack config to use module federation to be able to consume the modules.

new ModuleFederationPlugin({
  name: "home",
  library: { type: "var", name: "home" },
  remotes: {
    "mf-basket": "basket",
    "mf-productList": "productList",
  },
  shared: require("./package.json").dependencies,
});

As you can see, we have very much the same options from our two modules (e.g. name, library and shared). The key option the remotes section. Here we define the modules we are going to consume. In our case it is the basket and product list.

The remotes object key names follow a format in which the key name is the name we will use when the package is imported. Since these keys will be the import names, we prepend them with mf- to let us differentiate between node modules, internal modules, and module federated modules. Let’s now update the App.jsx file to import and use our modules.

const Basket = React.lazy(() => import("mf-basket/Basket"));
const ProductList = React.lazy(() => import("mf-productList/ProductList"));

Using React lazy to import the modules allows us to lazily load the components into our application. As our components are not bundled with the main application, we need to fetch them over the network at runtime. We don’t want to block the application rendering while these modules are being fetched. React lazy is a way to import a file or in this case a component over the network. Since we are using the React JS lazy import pattern we also need to use the React suspense component wrapper..

<section>
  <React.Suspense fallback={<div>....loading product list</div>}>
    <ProductList
        onBuyItem={onBuyItem}
    />
  </React.Suspense>
</section>
<section>
  {
    selected.length > 0 &&
    <React.Suspense fallback={<div>....loading basket</div>}>
      <Basket
        items={selected}
        onClear={() => setSelected([])}
      />
    </React.Suspense>
  }
</section>

React.suspense is a component from the React library that we can wrap a lazy loaded component with. This allows us to render a fallback or loading component. This is useful as the React.suspense component knows when the child has finished fetching/rendering and will render whatever is passed to the fallback prop until the child is available. In our case we are just providing some text, but this could be a React component of its own. Once the components are loaded, they will be rendered immediately and we can pass them props and treat them like any other React component.

Running the application

So now that we have built out our modules and our home container, let’s run the application to see how it all works in practice. Since we have all the parts in a single repo, we can use the npm scripts in the root of the repo to run all packages. We can achieve this by running npm start. This will trigger the start script for the home, basket and product list. It will also open all applications, including each separate micro front end, in separate browser tabs. We only want to use the application which is hosted on http://localhost:8080/. Once it has started, we can add products to our basket and see the application in action. If we look at the network panel we can see how the React lazy import function is loading in the components only when they are needed.

We can see here that we first load the main application, and that the Basket JSX file is loaded when we add an item to our basket. The micro frontend approach can greatly improve application performance as you are not downloading and executing code that users may not need, and you can keep your bundled JavaScript small. We can also see the Styled Components code being loaded. This is due to the shared option in the Webpack plugin. As we can tell the home app which code we share it knows when it needs to load dependencies for modules. The main benefit to this pattern is that when we want to make code changes to the basket, we can change the component code rebuild and deploy it without having to make any changes to the home application. Once the user refreshes the page they will get the latest version of the basket.

Conclusion

We can see the power that a micro frontend application can provide, when it comes to delivering the best performance to the end user and allowing multiple teams to build a deploy in parallel.

Do you have a large application that would benefit from being broken down into modules? Do you have multiple teams deploying to the same application? Then maybe a micro frontend architecture would help you.

Get started with Reflect today

Create your first test in 2 minutes, no installation or setup required. Accelerate your testing efforts with fast and maintainable test suites without writing a line of code.

Copyright © 2022 Reflect Software Inc. All Rights Reserved.