How To Share States Between React Micro-Frontends using Module-Federation?

Share states between React Micro-Frontends using Module-Federation and Bit!

Nitsan Cohen
Bits and Pieces

--

Micro-frontends offer a ground-breaking paradigm for building scalable and maintainable web applications by breaking down the monolithic architecture into manageable, independently developed components.

To find out more about micro-frontends, check out this post:

However, this doesn’t mean that micro-frontends are always beneficial.

One of the biggest drawbacks of using micro-frontends with React is sharing state sharing across these distributed units.

Unlike traditional monolith applications, micro-frontends have to grapple with synchronizing state across different origins and domains — a task that browsers aren’t inherently designed for.

You can see the deployed app here: https://mf-host-sharing-state.netlify.app/

In this article, we’ll explore the various strategies that let your micro-frontends seamlessly communicate with each other.

Additionally, we’ll utilize Bit to build our application, leveraging its flexibility to switch between runtime and build time integration, thus simplifying the complexity of managing micro-frontends.

The Task at Hand

I’ve created a comprehensive example hosted on this Bit Cloud scope: learnbit-react/sharing-state-mfe.

This scope includes a host-app component, which employs webpack-transformers/mfe-host-transformer for efficiently loading remote modules using Module Federation.

Under the remote-mfe you’ll find app components that are deployed independently to distinct URLs. Each component is also independently versioned with Bit.

This setup offers significant benefits, such as isolated development, separate deployment cycles, and granular version control for each micro-frontend, reflecting the flexibility and scalability of this architectural approach.

How To Share State Across Micro-Frontends?

Technique 01: Custom Events for State Sharing in Micro-Frontends

Custom events offer a practical solution for state sharing in web micro-frontends. This approach is highly scalable and aligns with event-driven architectures common in microservices.

For a hands-on example, consider our Planet Event Weather Component.

This component listens for a planetDataChange event and updates its state based on the event details. Such a method allows for efficient communication between components, ensuring synchronized updates across an application.

import { useEffect, useState } from 'react';

export const PlanetEventWeatherComponent = () => {
const [weatherData, setWeatherData] = useState({/* initial state */});

const handleCustomEvent = (event) => {
setWeatherData(event.detail);
};

useEffect(() => {
const handleCustomEvent = (event) => {
setWeatherData(event.detail);
};

window.addEventListener('planetDataChange', handleCustomEvent);

return () => {
window.removeEventListener('planetDataChange', handleCustomEvent);
};
}, []);

// ... (rendering UI)
};

export default PlanetEventWeatherComponent;

You can explore the full implementation and see this approach in the component’s code.

This technique is especially useful for dynamic data updates and interactive user experiences.

While custom events offer a robust solution for state sharing in web micro-frontends, they come with their own set of challenges:

  1. Limited Scope for Mobile Micro Frontends: Custom events are primarily designed for web applications. Implementing them in mobile micro frontends can be challenging, as they rely on browser-specific APIs.
  2. Dependency on Event Subscription Timing: Micro frontends need to subscribe to events before they are published for effective data transmission. This sequencing requirement can add complexity to the application’s architecture, especially in dynamic environments where components are frequently mounted and unmounted.
  3. Coordination Overhead: Ensuring all teams follow the custom event pattern consistently requires coordination and adherence to common standards. This can be a significant overhead in larger organizations with multiple teams working on different micro frontends.

Despite these challenges, custom events remain a valuable tool for web micro frontends, offering a native, browser-based solution for event-driven state management.

Technique 02: Message Bus for State Sharing in Micro-Frontends

The Message Bus technique is also known to event-driven architectures in microservices and facilitates state sharing in micro-frontends.

This pattern involves a centralized messaging system to publish and subscribe to events, promoting seamless data flow across components.

For instance, our Planet Message Bus Weather Component exemplifies this approach. It subscribes to a ‘planetData’ event through a shared message bus, ensuring real-time data synchronization across micro-frontends.

import { useEffect, useState } from 'react';
import { messageBus } from '@learnbit-react/sharing-state-mfe.shared.message-bus';

export const PlanetMessageBusWeatherComponent = () => {
const [weatherData, setWeatherData] = useState({/* initial state */});
useEffect(() => {
const handleDataUpdate = (data) => {
setWeatherData(data);
};
messageBus.subscribe('planetData', handleDataUpdate);
return () => messageBus.unsubscribe('planetData', handleDataUpdate);
}, []);
// ... (UI rendering)
};
export default PlanetMessageBusWeatherComponent;

In our implementation, both the remote and host components utilize the same instance of the shared message-bus component. This shared component, created with Bit, serves as the backbone for the message bus system, ensuring consistent communication across micro-frontends.

By leveraging Bit, we benefit from its capability to create consumable packages for each component. This simplifies the integration of the message bus method and ensures that both remote and host apps use the same instance, as defined in the Module Federation’s shared configuration:

'@learnbit-react/sharing-state-mfe.shared.message-bus': {
singleton: true,
requiredVersion: '^0.0.1',
eager: true,
}

The message bus technique is particularly beneficial for build-time integration in mobile micro-frontends, where custom events, typical in web applications, are absent.

Additionally, you must ensure that all your micro-frontends use the same message bus instance, as it serves as the sole registry for all published events, fostering efficient and consistent communication across components.

Bit significantly aids this process by offering run-time and build-time modules for consumption. This flexibility allows for seamless integration of shared components, like the message bus, during the build phase of the application. This adaptability is a key advantage when working with diverse architectures, such as mobile micro-frontends, ensuring a unified and efficient communication system across different parts of the application.

Challenges:

  • Requires a consistent implementation across all micro-frontends.
  • The need for a shared message bus instance can introduce complexity in certain architectures.
  • Coordination and standardization are essential to prevent fragmented communication strategies.

Technique 03: State Sharing through Props in Micro-Frontends State

Sharing through props is a fundamental and straightforward method in React micro-frontends. This technique involves passing data as props directly to the components. It’s particularly effective for static data or scenarios where parent-child relationships are transparent.

For instance, our Planet Weather Component illustrates this approach. It receives planetary data as props and renders the corresponding weather information.

This method is intuitive for developers familiar with React’s fundamental concepts.

// PlanetWeather.js
import React from 'react';

interface PlanetWeatherProps {
planetName: string;
sol: string;
temperature: string;
imageUrl: string;
}

export function PlanetWeather({
planetName,
sol,
temperature,
imageUrl,
}: PlanetWeatherProps) {
// Component logic and rendering
};
export default PlanetWeather;

While this approach is straightforward, it’s most effective when components maintain a direct parent-child relationship.

This method may become cumbersome in more complex scenarios involving deep nesting or sibling components, leading to ‘prop drilling’.

However, its simplicity and ease of understanding make it a go-to choice for many scenarios.

Explore the component in detail here.

Challenges:

  • Increased Coupling: Relies heavily on the container app for data flow, potentially reducing micro frontends’ independence.
  • Framework Restrictions: Effective implementation requires the same framework across all micro frontends, limiting flexibility.
  • Performance Overheads: This can lead to inefficient rendering cycles, especially with deep prop drilling in complex applications.

Technique 04: Platform Storage APIs for State Sharing in Micro-Frontends

Platform storage APIs like Local Storage offer a unique approach to state management in micro-frontends. This method allows micro frontends to set and read data independently directly, minimizing dependency on the container app.

For instance, our Planet Storage Weather Component utilizes Local Storage to persist and retrieve weather data, showcasing how micro frontends can effectively manage state in isolation.

// PlanetStorageWeatherComponent.js
import { useEffect, useState } from 'react';
import styles from './planet-weather.module.scss';
export const PlanetStorageWeatherComponent = () => {
const [weatherData, setWeatherData] = useState({/* initial state */});

useEffect(() => {
const updateFromStorage = () => {
const data = localStorage.getItem('planetWeatherData');
if (data) {
setWeatherData(JSON.parse(data));
}
};

updateFromStorage();
window.addEventListener('localStorageUpdated', updateFromStorage);

return () => {
window.removeEventListener('localStorageUpdated', updateFromStorage);
};
}, []);

// ... (rendering UI)

};

export default PlanetStorageWeatherComponent;

This technique is versatile and applicable in web and mobile contexts, with Local Storage for browsers and Async Storage for mobile apps. However, it comes with its own set of challenges:

  • Limited Scalability: More suitable for smaller datasets; managing large data sets can become complex and unwieldy.
  • Debugging Difficulty: Tracing which micro frontend is manipulating the data can be challenging.
  • Security Concerns: Not recommended for sensitive data due to potential security risks.

Despite these limitations, platform storage APIs remain a valuable tool, especially in scenarios where micro frontends operate on separate screens, allowing for efficient data retrieval upon mounting. With Bit, the integration becomes more manageable, offering seamless build-time and runtime solutions for various platforms.

Wrapping up

We’ve demonstrated effective strategies like:

  1. Custom Events
  2. Message Bus
  3. Props
  4. Platform Storage APIs

And, as expected, we have implemented this by leveraging React, Module Federation, and Bit.

It’s important to understand that each method offers unique advantages and challenges, therefore, it’s important you consider all the challenges, advantages presented in this article before making a decision for your next big project!

If you wish to explore the code we implemented, check out the full code here. Additionally, the link to the demo is attached below :)

Choosing between build time and runtime integration is a breeze: https://mf-host-sharing-state-build-time.netlify.app/

--

--

Making 𝘾𝙤𝙢𝙥𝙤𝙣𝙚𝙣𝙩-𝘿𝙧𝙞𝙫𝙚𝙣 Software a 𝐑𝐞𝐚𝐥𝐢𝐭𝐲 @ 𝐛𝐢𝐭.𝐝𝐞𝐯