Micro-frontends Made Easy

Agile Actors
PlayBook

--

By Senior Software Engineer — Sotiris Tsiamouris

This article is based on a presentation done earlier this year as an Agile Actors internal meetup event. The title is heavily (entirely) influenced by this great video of Jack Herrington that also demonstrates vite-plugin-federation. So, let’s dive into micro-frontends!

What are Micro-frontends?

Micro-frontend architecture is the application of the Microservices architecture in the frontend world. Basically, it’s a great way to decouple your application based on business capabilities.

Like Microservices, with Micro-frontends you split an application into smaller applications, or remotes, that are independently deployable and loosely coupled.

But splitting an entire application into smaller parts has some critical design considerations and there are things to consider priorly:

· Identifying micros

· Defining their responsibilities

· Their APIs and collaborations

If you get it wrong you risk creating a distributed monolith, which will slow down software delivery.

Decoupling a Monolith

Before we talk about decoupling let’s take some time to talk about Monoliths.

A Monolith is an application that consists of a single codebase and a single deployment. Most of the frontend applications out there are Monoliths, so if your application is small, has a single responsibility and there is a single team that manages it probably you are good with what you have and you could decouple your codebase a bit by applying shared assets techniques, e.g. having your design system and components in a separate codebase and use them as a library.

If it is hard to decide if your application needs to be split into decoupled frontends there is also this feature slider in microfrontend.dev that could probably help.

Back to decoupling.

There are multiple techniques to decouple a frontend, but let’s use an e-shop as an example to understand those techniques.

Consider the following e-shop a Monolith

Every component of this diagram is tightly coupled to each other. To complete a checkout, you will need to somehow interact with more than one of these components.

This coupling makes it difficult to change something without affecting the other components and that affects the time to market of your application and of course makes it hard to maintain. But if you decouple it, you could split a big problem into smaller ones and have separate teams that are proficient in parts of the application to work independently and release faster.

When it comes to decoupling a monolith application, there are multiple approaches, and the choice of one or the other will depend on the context, the business needs, the team’s maturity, and the technical debt.

Modulith (A Modular Application)

Split your application by breaking apart a monolith into modules that can be independently worked on. This is a valid approach to decouple a large application. The difference between a Modulith and a Micro-* distributed architecture, is that those loosely coupled modules are still typically deployed together.

Vertical Micro-frontends

When we’re decoupling a monolithic frontend or designing a composable system from the get-go, and we split it into multiple fully-fledged applications each loading at a different URL, we’re implementing a vertical split.

There are multiple tools and approaches that will help you achieve a vertical split (Next.js for example has Multi-Zones or you could just implement a reverse-proxy) and you should pick one, or combine some, based on your tech stack and needs.

Horizontal Micro-frontends

The horizontal split represents a pattern that mixes components developed, released, deployed and published by independent teams, but that are integrated into a single application route or view.

There are multiple tools and approaches that will help you achieve a horizontal split, but in this article, we will demonstrate the module federation approach.

Module federation is a feature, introduced in Webpack 5, that provides the ability to split an application into smaller, independently deployable modules that can be loaded on demand when they are needed.

At its core, Module Federation is based on the idea of remote loading of JavaScript modules. This means that instead of having all the code for a single application loaded at once, the code can be split into smaller, independently deployable modules that can be loaded on demand when they are needed.

Multiple separate builds should form a single application. These separate builds act like containers and can expose and consume code between builds, creating a single, unified application.

But enough with the theory, time for some code.

The example

As an example, a simple application was created, using vite and vite-plugin-federation. Vite was selected (over webapack) because of the ease of configuration and the rich modular ecosystem that provides.

The application consists of three remotes, a header, a footer and a counter and a single host. In module federation all the applications can share modules between them, but for the shake of the example, the host acts as the wrapper, the main application that renders all the micro-frontends and the remotes act as the independent applications that provide their modules.

The entire codebase is packed in a monorepo using pnpm workspaces for the ease of use and orchestration that workspaces provide.

All the remotes have similar configuration, this is header’s vite.config.ts:

export default defineConfig({
plugins: [
...
federation({
name: "header_app",
filename: "headerRemoteEntry.js",
exposes: {
"./Header": "./src/components/header/Header",
},
shared: ["clsx", "react", "react-dom"],
}),
],
...
});

The interesting parts of the above configuration are:

· exposes: the modules that are exposed by the remote application

· shared: The dependencies that are shared between the independent applications. If react is needed in more than one application, this module will be downloaded once and shared between them.

This is the host’s vite.config.ts:

export default defineConfig({
plugins: [
...
federation({
name: "app",
remotes: {
remoteHeader: "http://localhost:5001/assets/headerRemoteEntry.js",
remoteFooter: "http://localhost:5002/assets/footerRemoteEntry.js",
remoteCounter: "http://localhost:5003/assets/counterRemoteEntry.js",
},
shared: ["@reduxjs/toolkit", "react", "react-dom", "react-redux"],
}),
],
...
});

The interesting part of the above configuration is:

· remotes: The name and the location of the build of the application that exposes its modules

So, in general the remotes provide the modules, the host has the name and the location of the modules configured and they can be simply loaded into the host like any other module. Take as an example the host’s App.tsx file:

import { clsx } from "clsx";

// Remotes
import { Header } from "remoteHeader/Header";
import { Footer } from "remoteFooter/Footer";
import { Counter } from "remoteCounter/Counter";

import { withDarkMode } from "./components";
import Logo from "./mf.svg";

import styles from "./App.module.css";

const APP_NAME = "Module Federations Example";

type Props = {
darkMode: boolean;
};

const BaseApp = ({ darkMode }: Props) => (
<div className={clsx(styles.container, { [styles.darkMode]: darkMode })}>
<Header logoSrc={Logo} appName={APP_NAME} />
<main>
<p>This paragraph belongs to the host application</p>
<Counter />
</main>
<Footer appName={APP_NAME} />
</div>
);

const App = withDarkMode(BaseApp);

export default App;

So, we’ve seen that it’s pretty easy to share and load modules between different applications, but if we want to have a valid Micro-frontends example and since state management is one of the most known challenges of this architecture, two implementations will be presented on how we can “share” a modules state.

Redux injected reducers

Considering that all the micros are using Redux, and the store of the application is managed by the host, each micro-frontend can inject its reducer to this store, like this module-federation github example.

In our case, this module is the Counter, and as we can see, when the increment button is clicked an action is being dispatched and the state and view of our host application changes.

The reducer injection is provided by the withModelActivation.tsx higher order component.

import { type Reducer } from "@reduxjs/toolkit";
import {
type ComponentType,
type ContextType,
useEffect,
useContext,
} from "react";
import { ReactReduxContext } from "react-redux";
import { injectReducer, type EnhancedReduxStore } from "./injectReducer";

export type ModelActivationOptions = {
reducer: { name: string; value: Reducer };
};

export const withModelActivation =
(options: ModelActivationOptions) =>
<P extends object>(Component: ComponentType<P>) =>
(props: P) => {
const { store } = useContext(ReactReduxContext) as ContextType<
typeof ReactReduxContext
> & { store: EnhancedReduxStore };

const {
reducer: { name, value },
} = options;

useEffect(() => {
!!store && !!value && injectReducer(store, name, value);
}, [store, name, value]);

return <Component {...props} />;
};

That uses the injectReducer.tsx helper function.

import {
combineReducers,
type Action,
type Reducer,
type Store,
type UnknownAction,
} from "@reduxjs/toolkit";

export type EnhancedReduxStore<
// eslint-disable-next-line @typescript-eslint/no-explicit-any
S = any,
A extends Action<string> = UnknownAction,
StateExt = unknown
> = Store<S, A, StateExt> & { asyncReducers: Record<string, Reducer> };

export const injectReducer = <RootStore extends EnhancedReduxStore>(
store: RootStore,
name: string,
reducer: Reducer
) => {
store.asyncReducers = {
...(store.asyncReducers ?? {}),
[name]: reducer,
};

store.replaceReducer(combineReducers(store.asyncReducers));
};

(I know that I must change the S parameter default type 😀 )

But the nice thing with micro-frontends is that you can have different teams that can use the technologies and the tools that the teams feel comfortable with, so we should provide a more generic example on how you can share state between the micros.

Publish Subscribe

An elegant way to have your micros communicate between them is publish subscribe, that way a micro can communicate a state change to the entire application and the micros that are interested in this change be informed of this change.

In our example we have implemented a simple mechanism that uses postMessage for publishing events that can be subscribed to via a simple addEventListener.

Our publisher can be found in the withDarkMode.tsx higher order component that the header is using

import { type ComponentType, useState } from "react";

export const withDarkMode =
<P,>(Component: ComponentType<P>) =>
(props: Omit<P, "darkMode" | "toggleDarkMode">) => {
const [darkMode, setDarkMode] = useState(false);

const toggleDarkMode = () => {
const newMode = !darkMode;

setDarkMode(newMode);

window.postMessage({ type: "DARK_MODE", payload: newMode }, "*");
};

return (
<Component
{...(props as P)}
darkMode={darkMode}
toggleDarkMode={toggleDarkMode}
/>
);
};

And the subscriber can be found in higher order components inside the other applications with the same name, for example the withDarkMode.tsx higher order component of the footer micro.

import { type ComponentType, useState } from "react";

export const withDarkMode =
<P,>(Component: ComponentType<P>) =>
(props: Omit<P, "darkMode">) => {
const [darkMode, setDarkMode] = useState(false);

window.addEventListener("message", (event) => {
if (event.data.type === "DARK_MODE") {
setDarkMode(event.data.payload);
}
});

return <Component {...(props as P)} darkMode={darkMode} />;
};

As the name of those higher order components implies, the header micro is responsible for the color mode of the application and the other micro’s need to be informed for this change to change their color mode too.

Wrapping up

Micro-frontend architecture will introduce new challenges to your team, and it is critical to decide if micro-frontends are a suitable solution for your problem,

Pros

· Scalability: Each frontend application or component can scale independently

· Independent deployability: Each frontend application or component can be developed and deployed independently, and may be able to fail in isolation

· Team independence: Each team developing a micro-frontend can make their own architecture and tech stack decisions

· Faster time to value: Independent deployability and teams, make it possible for them to iterate and ship faster,

· Cost efficiency: Scaling horizontally may reduce the total cost of ownership and computing, or capacity needs of the whole application

Cons

· Governance: For the implementation -including the UI definitions- to remain consistent across the whole application, particularly across the many views when that may be a requirement, and for certain conventions to be respected there should be a strong and strict governance definition that may need to be enforced to all teams

· Orchestration: Orchestrating the deployment of decoupled components or micro-parts may require additional efforts, and even an additional piece of technology to maintain

· State management: State management is potentially the most challenging aspect of composition, since an out of sync state can cause the application to behave erratically and even expose security or privacy flaws

· Routing: Routing is one of the common challenges of application design, and it’s a very important aspect of both user-experience and data architecture. It is even more challenging when we’re orchestrating decoupled frontend experiences, particularly when the routes of nested components may need to be exposed to the user

· Data architecture: Together with state management, data architecture, particularly for transactional workloads, can suppose a major challenge

But if you decide to go with micro-frontends, vite-plugin-federation is a great tool to use.

Resources

· https://microservices.io/

· https://microfrontend.dev/

· https://micro-frontends.org/

· https://module-federation.io/

· https://webpack.js.org/concepts/module-federation/

· https://github.com/originjs/vite-plugin-federation

· https://dev.to/marais/webpack-5-and-module-federation-4j1i

Example’s repository: https://github.com/zotijs/vite-micro-frontends

Presentation: https://zotijs.github.io/vite-micro-frontends/

If you’re passionate about all things Frontend, check out our exciting opportunities here!

--

--