Skip to content

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

In client-side composition, each micro frontend is dynamically assembled by the client (e.g. the browser).

There are different ways to implement this:

  • using frameworks, like SingleSPA or Qiankun for Single Page Applications, or Luigi for iframes;
  • using Module Federation.

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:

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 artifact 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 artifact 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 artifact 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 organized 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

The exported Loading component is quite easy:

Plain Text
import { useState, useEffect } from 'react'
 
const Loading = () => {
 const [tickedTimes, setTickedTimes] = useState(0)
 useEffect(() => {
   const interval = setInterval(
     () => setTickedTimes(tickedTimes => tickedTimes + 1),
     500
   )
   return () => clearInterval(interval)
 }, [])
 
 return `Loading ${'.'.repeat(tickedTimes)}`
}
 
export default Loading

So, let’s focus on module federation configuration :

Plain Text
const deps = require('./package.json').dependencies
 
module.exports = {
 name: "loading",
 filename: "loading.js",
 exposes: {
   './Loading': './src/Loading.jsx',
 },
 shared: {
   ...deps,
   react: { singleton: true, requiredVersion: deps.react },
   "react-dom": { singleton: true, requiredVersion: deps['react-dom'] }
 },
}

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 :

Plain Text
import ButtonCss from './Button.module.css'
 
const Button = props => (
 <button className={ButtonCss['federated-button']} {...props}>
   {'Federated button'}
 </button>
)
 
export default Button

And a styled version of the Loading component , imported from the loading remote:

Plain Text
import Loading from 'loading/Loading'
import ColorLoadingCss from './ColorLoading.module.css'
 
const ColorLoading = () => (
 <div className={ColorLoadingCss['loading-container']}>
   <Loading />
 </div>
)
 
export default ColorLoading

Now, let’s move to the module federation configuration :

Plain Text
const deps = require('./package.json').dependencies
 
module.exports = {
 name: 'components',
 filename: 'components.js',
 exposes: {
   './ColorLoading': './src/components/color-loading/ColorLoading.jsx',
   './Button': './src/components/button/Button.jsx'
 },
 remotes: {
   loading: 'loading@http://localhost:3002/loading.js'
 },
 shared: {
   ...deps,
   react: { singleton: true, requiredVersion: deps.react },
   'react-dom': { singleton: true, requiredVersion: deps['react-dom'] }
 }
}

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 time, for the source code, take a look only at the App file content .

It contains the components imported from the bidirectional host :

Plain Text
import './App.css'
 
import ColorLoading from 'components/ColorLoading'
import Button from 'components/Button'
import { useEffect, useState } from 'react'
 
function App() {
 const [isLoading, setIsLoading] = useState(true)
 
 useEffect(() => {
   if (isLoading) {
     setTimeout(() => setIsLoading(false), 3000)
   }
 }, [isLoading])
 
 return (
   <div className="App">
     <header className="App-header">
       <div>
         <Button onClick={() => setIsLoading(true)} disabled={isLoading} />
         {isLoading && <ColorLoading />}
       </div>
     </header>
   </div>
 )
}
 
export default App
Plain Text
const deps = require('./package.json').dependencies
 
module.exports = {
 name: "application",
 remotes: {
   components: "components@http://localhost:3001/components.js",
 },
 shared: {
   ...deps,
   react: {singleton: true, requiredVersion: deps.react},
   "react-dom": {singleton: true, requiredVersion: deps['react-dom']}
 },
}

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 artifacts to release;
  • more immediate rollbacks because we only have to re-expose the old federated artifact 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.

Need help with Frontend development ? Contact us today to organize a discovery workshop .

Insight, imagination and expertly engineered solutions to accelerate and sustain progress.

Contact