React Micro frontends with Module Federation
Understand what Micro Frontends are and why Module Federation is useful
The idea behind micro frontends is to think about a website or web app as a composition of features which are owned by independent teams. Each team has a distinct area of business or mission it cares about and specialises in. A team is cross-functional and develops its features end-to-end, from database to user interface.
Micro Frontends 101
Micro frontends are an emerging architecture inspired by microservices. Building with micro frontends is a modern strategy for deconstructing a monolithic codebase into smaller parts, in an effort to increase the autonomy of each team and the delivery throughput. Each part can be considered a micro frontend, which will be deployable independently of the others. In this post, we will be looking at building a React application that leverages micro frontends with module federation.
Micro Frontend Approaches
Before deconstructing the monolith, you should choose the right approach to split your pages. This is useful to understand better and segregate the domain of your application (e.g. login page, private area…). The most common approaches are vertical split (page-based) and horizontal split (parcel-based).
Micro Frontend Approach: Vertical Split
A vertical split approach is the easiest as it allows you to have only one micro frontend at a time on each page. This results in the closest developer experience to a Single Page Application in terms of tools, patterns, and best practices, which remain the same.
Micro Frontend Approach: Horizontal split
A horizontal split allows you to have multiple micro frontends at a time in each view. Usually, it is the most common strategy in business domains with multiple reusable parts.
Micro Frontends Composition
Once you have chosen the right split strategy and created your micro frontends, there is one last thing to do: decide how to compose them. The available techniques are client-side, edge-side, and server-side.
Composing Micro Frontends: Client-side
Composing Micro Frontends: Edge-side
In edge-side composition, each micro frontend is assembled by the edge (e.g. CDN) using the Edge Side Include (ESI) specification. It is not supported by all the CDNs, and each vendor (Akamai, CloudFlare, Fastly…) behaves differently.
Composing Micro Frontends: Server-side
In server-side composition, each micro frontend is assembled by the server. Even in this case, there are ways to implement it:
using frameworks, like Ara Framework, Open Components, or Piral;
using Module federation.
Module Federation Basic Concepts
The primary use case for module federation is the distribution of software at runtime, with additional support for fallbacks. It works on both client and server. Although it was born as a Webpack Plugin, now communities are trying to implement the same principles for the other bundlers (Rollup, Vite…).
Common Module Federation Terminology
Here is a list of terms commonly used, that will be useful to better understand the rest of the article:
Host
The host is an artefact that includes the initial chunks of our application, the ones that will be used to bootstrap it. The bootstrap phase is pretty common, and starts once the window.onload event has been triggered. The biggest difference lies in the management of the components: normally they are included in the application bundle, increasing its size. With module federation, they are not embedded in the bundle but are just referenced to a remote.
This allows us to have a smaller bundle size and a reduced initial load time. Note that as an application has multiple dependencies, a host can also have multiple remotes.
Remote
The remote is an artefact that provides the code to be consumed by the host. The provided code can be both shared components or common dependencies which will be used by different hosts.
Bidirectional host
The bidirectional host is an artefact that can be both a host or a remote, consuming other applications or being consumed by others .
Federated
The federated objective applies to the code which uses the module federation.
A Real World Scenario
Now, we are going to create an example project to replicate the scenario of the following picture:
In this scenario, we have a host called application, a bidirectional host called components, and a remote called loading. In plain English: firstly, the remote will expose a single loading component. Secondly, the bidirectional host will import the loading component, apply some CSS and then re-expose it. Additionally, it exposes a rounded button.
Finally, the host will include these two components that will be shown to the user. Before diving into the code, you should know that:
all the source code related to this example will be available at nearform/module-federation-example;
this example uses a horizontal split with client-side composition;
each package in the monorepo has been generated using create-react-app and then extended with craco to override the Webpack configuration using a dedicated plugin that reads and applies the module federation configuration of each package;
although the provided code is organised as a monorepo, the same idea works with multiple repos or any other kind of development strategy;
each federated module can be used as an independent React application.
The Loading Remote Implementation
So, let’s focus on module federation configuration:
It defines that:
We are creating a remote called loading.
We are sure this is just a remote because we are just exposing something;
this remote will be loadable using an entry point called loading.js;
it exposes our Loading component;
each dependency defined in the package.json will be shared with the other federated modules.
Additionally, we ensure that a specific version of react and react-dom are instantiated just once.
When it is loaded individually, it renders only the Loading component:
The Components Bidirectional Host Implementation
The bidirectional host exports a rounded button:
And a styled version of the Loading component, imported from the loading remote:
Now, let’s move to the module federation configuration:
This time, it defines that:
We are creating a bidirectional host called components.
We are sure this is a bidirectional host because we are defining both the exposed components and the remotes to import;
this bidirectional host will be loadable using an entry point called components.js;
it exposes the rounded Button component and the styled version of the Loading component;
it loads its dependencies from a remote called loading, the entry point of which is reachable at the address http://localhost:3002/loading.js;
each dependency defined in the package.json will be shared with the other federated modules.
Additionally, we ensure that a specific version of react and react-dom are instantiated just once.
When it is loaded individually, it renders the exported components:
The Application Host Implementation
This is the last module federation configuration:
It defines that:
We are creating a host called application.
We are sure this is a host because we are defining just the remotes to import. Please also note the absence of the filename configuration;
it loads its dependencies from a remote called components, which entry point is reachable at the address http://localhost:3001/components.js;
each dependency defined in the package.json will be shared with the other federated modules.
Additionally, we ensure that a specific version of react and react-dom are instantiated just once.
In the end, this is what the user will see:
Module Federation: the Trade-Offs
Applying module federation we have found some trade-offs, which will be explained below:
Pros
Exposing parts of the application in such a dynamic way allows us to have:
faster builds as there is less code to process inside a single package;
more rapid deployments because there are fewer artefacts to release;
more immediate rollbacks because we only have to re-expose the old federated artefact to deploy an old version of the application.
Cons
Module federation is a panacea that can be used everywhere, you must know the shape of your application really well to apply it correctly. As you can see, it also requires a little bit of configuration, which might not be so familiar to developers that have always worked with SPA.
Risks
Without a proper study of the size of each remote or bidirectional host, the final result can be compromised due to the high number of network calls to retrieve each JavaScript file. You should avoid creating lots of small files.
Insight, imagination and expertly engineered solutions to accelerate and sustain progress.
Contact