Kapeel Kokane Coder by day, content creator by night, learner at heart!

Build progressive micro-frontends with Fronts

6 min read 1817

Micro Frontends Frontsjs

The way that we develop applications has changed a lot over the past few years. With new frameworks being released daily, having a flexible tech-stack, for example, building some parts of your app in React and others in Vue, is highly beneficial when working across teams.

Micro-frontends allow us to build web apps that are developed and deployed separately but work as a single app. Micro-frontends are one common approach to achieving greater flexibility, allowing teams to merge components built in different frameworks or libraries. In production, there are several different ways to implement micro-frontends.

In this tutorial, we’ll explore a few methods for building micro-frontends. Then we’ll look into Fronts.js in-depth, a progressive micro-frontend framework for building web applications. Let’s get started!

Alternatives to Fronts

First, let’s review two common approaches to building micro-frontends. web

single-spa

Single Spa Router

The single-spa router uses a single-spa root config file, which holds information like shared dependencies, as well as several individual SPAs packaged into modules that communicate with each other and single-spa via APIs.

webpack Module Federation

Another popular solution is webpack’s Module Federation, which was introduced as one of the core features in webpack v.5. Module Federation solves the problem of code sharing by allowing applications to load code from each other. However, Module Federation is a bit verbose and lacks the option for customization.

Getting started with Fronts

In comparison to other micro-frontend frameworks, Fronts provides a more complete and fully-targeted implementation, offering several unique benefits like support for nested micro-frontends. A nested micro-frontend is a frontend built with one codebase that is placed inside another frontend built with a different codebase. Nested micro-frontends allow a parent page to hold several different child pages that could each potentially be built using a different repository.

Additionally, Fronts offers cross-framework support, meaning developers are not restricted from using any particular tech stack. Cross-framework support also allows developers to build a single page using several different technologies. Code splitting and lazy loading are two additional popular features included in Fronts, which allow different apps that are built using Fronts to import different Fronts apps as modules.

We can hook into the lifecycle methods provided by Fronts apps and perform the required operations. Finally, the Fronts API is somewhat generic and framework agnostic, meaning we can apply it to a wide variety of use cases.

We made a custom demo for .
No really. Click here to check it out.

Fronts progressive nature

Fronts is progressive in nature, meaning it supports both Module Federation and non-module-federation. An application that starts off in a normal mode without Module Federation can progressively shift into Module Federation.

In addition to Module Federation, Fronts offers a version control mode that allows version management of the different component applications.

Looking into the Fronts API

As we mentioned before, the Fronts API is pretty straightforward. However, it’s worth taking an in-depth look at its code. Fronts includes three different loaders, selecting the correct one based on the requirement.

When we need to isolate the CSS from other applications, we use the useWebComponents() API. For applications that don’t require us to isolate the CSS from the other applications, we can use the useApp() loader, however, doing so is optional on an opt-in basis. Lastly, we use the useIframe() loader when we need to isolate both CSS and JavaScript from the rest of the application.

In addition to these three loaders, Fronts also includes two APIs for Module Federation.  createWebpackConfig() is a wrapper function that takes in the original webpack config and returns the updated webpack config that supports Module Federation. To generate the webpack config for Module Federation, simply call createWebpackConfig().

getMeta() is a tool used to get the full picture of the dependency map. Calling getMeta() returns a JSON of the entire monorepo structure, along with all the component apps and their dependencies.

Finally, Fronts provides APIs for communicating among the different component micro-frontends. globalTransport.listen helps configure global event listeners by providing a string event and a listener that is triggered when that event is fired.

The globalTransport.emit function emits events that the previous function will listen to. It takes a string event name as the first argument and its value as the second argument.

Example micro-frontend with Fronts

Now that we understand the essentials of Fronts, let’s run through an example to understand Fronts in greater depth.

For our demo, we’ll work on top of the official fronts-example repo. The container ecommerce site is the parent, and the products page is the child. To follow along, you can access the completed example repo. It uses Chakra UI for its UI components and has a hardcoded products.json file along with a dummy cart.

Our final micro-frontend example will look like the image below:

Micro Frontend Ecommerce Example

To run the application, clone the repository, then run the following command:

yarn install

Next, run the code below:

start

To play around with and test the application, visit localhost.

Fronts folder structure

The example repo above contains two full-fledged Fronts projects inside of the packages directory. Keep in mind that it’s not necessary to place the two projects inside of the same monorepo. Our application would work exactly the same way if each one were placed in its own separate repository.

The folder structure of each individual app looks roughly like the following code segment:

|- public
 |- index.html
|- src
    |- App.jsx
    |- index.jsx
    |- styles.css
    |- bootstrap.tsx 
|- .babelrc
|- webpack.config.js
|- site.json

Let’s look at each specific file in greater detail.

bootstrap.tsx

As the name suggests, we’ll use the bootstrap.tsx file to help us bootstrap our Fronts application when it is loaded in the browser:

export default function render(element: HTMLElement | null) {
  ReactDOM.render(<App />, element);
  return () => {
    ReactDOM.unmountComponentAtNode(element!);
  };
}
boot(render, document.getElementById('root'));

Notice how the app is rendered inside of a render function, then that render function is supplied to the boot method provided by the Fronts library.

Inside of the function, the React app is rendered as we would a normal React app. It also returns an arrow function that calls the ReactDOM.unmount on the element when it is no longer required.

As a user of the Fronts library, all we need to do inside of the bootstrap.tsx file is call the boot method, and Fronts will take care of the rest.

site.json

site.json is another important file that specifies a lot of configuration details for the app. Here’s how it looks for app1, the container ecommerce app:

{
  "name": "app1",
  "dependencies": {
    "app2": "http://localhost:3002/remoteEntry.js"
  },
  "shared": {
    "react": { "singleton": true },
    "react-dom": { "singleton": true }
  }
}

Notice how app2, the products app, is mentioned as one of the dependencies of app1 by passing the localhost deployment URL, followed by remoteEntry.js as an entry point. When we deploy both apps to production, remoteEntry.js will be replaced by the production deployment URL.

The shared object mentions all the dependencies shared by these Fronts applications so that the bundles are not downloaded again on the browser. Specifying the entry point and the shared dependencies in this format makes our bundle function in sync with other Fronts applications, and the library will take care of the rest for us.

Looking at the same file in app2, we see that it looks slightly different:

{
  "name": "app2",
  "exports": ["./src/bootstrap", "./src/Button"],
  "dependencies": {},
  "shared": {
    "react": { "singleton": true },
    "react-dom": { "singleton": true }
  }
}

Specifically, we see an extra exports key. This is because app2 exports certain functionality that can be used by other applications. In the example above, the src/bootstrap file is exported, meaning that the entire app can be imported in other Fronts applications like it was by app1.

Routing between micro-frontends

If you take a look at the app1/src/App.tsx file, you’ll see how the routes are being shared by the two applications:

const routes = [
  {
    path: "/",
    component: () => <HomePage />,
    exact: true,
  },
  {
    path: "/app2",
   component: () => {
     const App2 = useApp({
       name: "app2",
     loader: () => import("app2/src/bootstrap"),
      });
      return <App2 />;
    },
    exact: true,
  },
];

The route / is for the homepage, which is handled by the HomePage component present in app1:

Homepage Component Display

In the image shown above, the Home button and the Cart button come from the <Navigation /> component present in app1. The entire pink region is rendered by the <HomePage /> component.

Notice that the route /app2 is being handled by a component that uses the useApp functionality to generate a component from a separate micro-frontend. If we click on the Browse products button on the previous page, we are taken to the products page, seen below:

Ecommerce App Gif

The app that displays the three products inside of the light grey background is an entirely different app that is being seamlessly rendered by Fronts.js. We’ll see the proof inside the network tab when we see a call to the bootstrap function of app2:

Final Fronts Micro Frontend Example

Communication between micro-frontends

You might have noticed that upon clicking on the ADD TO CART button, the number shown at the top of the cart icon increments or decrements accordingly. Given that the two components are theoretically in two different micro-frontends, how are they communicating?

The communication happens courtesy of the globalTransport functionality exposed by the Fronts library. Inside app1/src/App.tsx, we’re setting up two global listeners for the increase and the decrease events respectively and modifying the cart count based on it:

const [count, setCount] = useState(0);
useEffect(
  () =>
    globalTransport.listen("increase", () => {
      setCount(count + 1);
    }),
  [count]
);
useEffect(
  () =>
    globalTransport.listen("decrease", () => {
      setCount(count - 1);
    }),
  [count]
);

Inside of the app2/src/App.tsx, we’re emitting the particular events when the corresponding buttons are being clicked:

function addToCart(pid) {
  const newProd = [...prod];
  newProd.forEach(p => {
    if (p.id === pid) {
      if (!p.active) {
        globalTransport.emit("increase");
        p.active = true;
      } else {
        globalTransport.emit("decrease");
        p.active = false;
      }
    }
  });
  setProducts(newProd);
}

Fronts is ensuring that the events triggered in one of the micro-frontends, the emitter, invoke the corresponding listeners in a different micro-frontend, the listener.

Differences from webpack

The Fronts library is written on top of webpack’s Module Federation in an attempt to simplify the process of building micro-frontends. Therefore, Fronts includes all the benefits of webpack out of the box. But when we compare both side-by-side, we see that Fronts has the following advantages:

  • Reduced configurations
  • No need to deal with plugin modifications
  • No need to deal with the raw webpack config
  • Simplified multi-app routing
  • A choice of CSS boundary level

Conclusion

Micro-frontends, which allow developers to work on parts of frontend applications separately and deploy them independently,continue to boom in popularity. Fronts truly shines by allowing developer teams to decouple the lifecycles of separate yet related applications without fearing breakages.

In my opinion, Front’s minimal configuration and its flexibility are currently unmatched. It is definitely worth considering as a framework of choice for your next micro-frontends project. I hope you enjoyed this tutorial.

Are you adding new JS libraries to improve performance or build new features? What if they’re doing the opposite?

There’s no doubt that frontends are getting more complex. As you add new JavaScript libraries and other dependencies to your app, you’ll need more visibility to ensure your users don’t run into unknown issues.

LogRocket is a frontend application monitoring solution that lets you replay JavaScript errors as if they happened in your own browser so you can react to bugs more effectively.

https://logrocket.com/signup/

LogRocket works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app’s performance, reporting metrics like client CPU load, client memory usage, and more.

Build confidently — .

Kapeel Kokane Coder by day, content creator by night, learner at heart!

Leave a Reply