Communicating between Micro Frontends using Custom Events in React.JS
As a part of a series of tutorials about Micro Frontends, in this tutorial, I’m going to explain how to communicate between React.JS MFEs using events.
I will explain why this is the best solution out there nowadays, and what are the pros and cons of using this methodology as well as display an example of usage.
Background:
In a micro frontend architecture, there should be a shell application that hosts the internal applications and displays them.
While using this architecture, means that the shell application is not managing the MFEs and that the MFEs are not sharing their states with the shell application or with any of the other MFEs hosted there, by that I mean that you can’t really use any state management such as Redux, MobX (for React) to manage the global states, because when using micro frontends we don’t want the applications to be coupled.
Solution:
As a solution, we’re going to use browser events, and create a shared service that will be used by applications to send the events or register to them.
This solution is more scalable technique for run-time MFEs. The main idea here is to utilize the browser-inbuilt custom events APIs to publish the events with the data from one MFE, while the other MFE subscribes to the events to get the data. This solution is closest to the event-driven architecture in the microservices world.
Pros:
- One of the most common ways to communicate using
eventListeners
andCustomEvent
- High setup cost but very easy to scale.
- The inbuilt solution in-browser platform.
- Build a generic mechanism that all the MFEs teams can follow.
- Framework agnostic (Means it can be implemented even if each of your MFEs uses a different framework)
Cons:
- Not achievable in the case of mobile MFEs.
- Verbose custom events API.
Implementation:
In the following example, I’m going to show you how to let each of the MFEs fire a Snackbar event to the shell application, and by doing it this way, we will have to implement that Snackbar only inside the shell application instead of implementing it in each of the MFEs.
First, we have to create a Window Event service that allow to fire events, subscribe, and unsubscribe to events.
export interface ISnackbarEvent {
isOpen: boolean;
message: string;
type: 'error' | 'success';
}export enum WindowEvents {
SNACKBAR = 'snackbar',
}export const WindowEventService = {
fire: (event: WindowEvents, body?: CustomEventInit): void => {
const customEvent = new CustomEvent(event, body);
window.dispatchEvent(customEvent);
},
subscribe: (event: WindowEvents, listener: EventListener) => {
window.addEventListener(event, listener);
},
unsubscribe: (event: WindowEvents, listener: EventListener) => {
window.removeEventListener(event, listener);
},
};
In the code above you can see how I described the Snackbar Event Interface which uses an isOpen property to know if it should be opened, a message which is a string, and a type that can be an error or success and will display a green or red background according to that.
I’ve also created a WindowEvents enum which should hold all the available events since we need them for both the application that subscribes to the event, and for the application that fire the event, and added SNACKBAR event into it.
Then, I created WindowEventService which is responsible for fire, subscribing, and unsubscribing to events.
Subscribing to an Event:
Now, let's subscribe to the Snackbar event in the shell application:
import React, { useEffect, useState } from 'react';
import { Snackbar } from 'shared-lib/Snackbar';
import {
WindowEvents,
WindowEventService,
ISnackbarEvent,
} from 'shared-lib/services';
import './style.scss';
const SnackbarDefaultValue: ISnackbarEvent = {
isOpen: false,
message: '',
type: 'success',
};
export function App() {
const [snackbar, setSnackbar] =
useState<ISnackbarEvent>(SnackbarDefaultValue);
useEffect(() => {
WindowEventService.subscribe(WindowEvents.SNACKBAR, updateSnackbar);
return () => {
WindowEventService.unsubscribe(WindowEvents.SNACKBAR, updateSnackbar);
};
}, [updateSnackbar]); const updateSnackbar = (event: Event) => {
const { detail } = event as CustomEvent<ISnackbarEvent>;
setSnackbar({ ...snackbar, ...detail });
}; const onCloseSnackbar = () => {
setSnackbar({ ...snackbar, isOpen: false, message: '' });
};
return (
<div className="app-container">
<div className="snackbar-container">
<Snackbar
content={snackbar.message}
type={snackbar.type}
isOpen={snackbar.isOpen}
onClose={onCloseSnackbar}
/>
</div>
</div>
);
}
export default App;
In the shell application, I used the subscribe function to subscribe to the snackbar event, and passed the updateSnackbar function which will be responsible to get the event from the service and handle it.
I’ve also cleaned the subscription on unmount using the unsubscribe function.
Firing an Event:
import React from 'react';
import { ISnackbarEvent, WindowEvents, WindowEventService } from 'shared-libs/services';
import './index.scss';
const App = () => {
const triggerSnackbar = () => {
WindowEventService.fire(WindowEvents.SNACKBAR, { detail: {
isOpen: true,
message: 'Operation succeed',
type: 'success',
}});
};
return (
<div className="app-container">
<button onClick={fireSuccessSnackbar}>
Fire Snackbar
</button>
</div>
);
};
export default App;
In the following code, you can see that all we have to do to fire an event is to send the event data inside the detail object, and then, if the shell application (or any other application) subscribed to that SNACKBAR event, they will receive the data.
That's it, your micro frontends now can communicate with each other, they don’t even have to know them, and they are still loosely coupled.
If you want to learn more, lookup for my previous articles: