One of the key challenges when implementing micro frontends is how to enable seamless communication between them. Since each micro frontend is developed and deployed independently, they must be able to interact with one another while maintaining their autonomy. In a micro frontend architecture, communication could be needed in scenarios such as sharing state, passing events, or calling APIs between different micro frontends.
Below, we’ll explore several methods and approaches to enable communication between micro frontends in a scalable and maintainable way.
Key Communication Approaches for Micro Frontends
- Custom Events (DOM Events)
- Shared State Management
- Message Bus (Pub/Sub) System
- URL Parameters/Query Strings
- Global State Management Libraries
- API Integration
1. Custom Events (DOM Events)
Custom DOM events are a simple and lightweight method for communication between micro frontends. Micro frontends can dispatch custom events that other micro frontends can listen to. This approach works well when you don’t need a deep or complex integration and just need to communicate at a high level.
Example:
One micro frontend can emit a custom event when an action occurs, and another micro frontend can listen for that event and respond accordingly.
Emitter Micro Frontend (Frontend A):
// Dispatch a custom event when a button is clicked
const button = document.getElementById('button');
button.addEventListener('click', () => {
const event = new CustomEvent('myCustomEvent', {
detail: { message: 'Hello from Micro Frontend A' },
});
window.dispatchEvent(event);
});
Listener Micro Frontend (Frontend B):
// Listen for the custom event and react to it
window.addEventListener('myCustomEvent', (event) => {
console.log(event.detail.message); // Output: "Hello from Micro Frontend A"
});
Advantages:
- Simple and easy to implement.
- No need for complex frameworks or libraries.
- Event-driven approach.
Disadvantages:
- Doesn’t scale well for more complex or frequent communication.
- Can get difficult to manage and track events in larger applications.
2. Shared State Management
Shared state is often necessary when multiple micro frontends need to share data. Instead of each micro frontend maintaining its own local state, they can access a shared global state. This can be done through various libraries or patterns such as Redux, React Context, or a custom state management solution.
Example:
Using React Context or a global store to share data across micro frontends.
State Provider (Shared Store):
// Using React Context to manage shared state
import React, { createContext, useContext, useState } from 'react';
const SharedStateContext = createContext();
export const SharedStateProvider = ({ children }) => {
const [message, setMessage] = useState('Initial State');
return (
<SharedStateContext.Provider value={{ message, setMessage }}>
{children}
</SharedStateContext.Provider>
);
};
export const useSharedState = () => useContext(SharedStateContext);
Micro Frontend A:
// Micro Frontend A can update the shared state
import React, { useEffect } from 'react';
import { useSharedState } from './SharedStateProvider';
const MicroFrontendA = () => {
const { setMessage } = useSharedState();
useEffect(() => {
setMessage('Updated from Micro Frontend A');
}, [setMessage]);
return <div>Micro Frontend A</div>;
};
export default MicroFrontendA;
Micro Frontend B:
// Micro Frontend B reads from the shared state
import React from 'react';
import { useSharedState } from './SharedStateProvider';
const MicroFrontendB = () => {
const { message } = useSharedState();
return <div>Message: {message}</div>;
};
export default MicroFrontendB;
Advantages:
- Centralized state management, which simplifies complex state-sharing.
- Scalable and easy to maintain.
- Works well with frameworks like React, Angular, or Vue.
Disadvantages:
- Increases complexity if not handled carefully (e.g., managing state across multiple micro frontends).
- Requires additional setup (e.g., creating global stores or providers).
3. Message Bus (Pub/Sub)
A message bus or pub/sub (publish/subscribe) system is a pattern where micro frontends can subscribe to certain events and publish events that others can listen to. This pattern decouples the micro frontends from each other and provides flexibility in communication.
You can use simple JavaScript-based event emitters or even a more advanced messaging system like RxJS, Redux-Observable, or EventEmitter.
Example (Using an Event Emitter):
// Event bus with EventEmitter
const EventEmitter = require('events');
const eventBus = new EventEmitter();
// Micro Frontend A publishes an event
eventBus.emit('message', { message: 'Hello from Micro Frontend A' });
// Micro Frontend B listens for that event
eventBus.on('message', (data) => {
console.log(data.message); // Output: "Hello from Micro Frontend A"
});
Advantages:
- Decouples micro frontends from one another.
- Allows asynchronous communication.
- Scalable and flexible for complex interactions.
Disadvantages:
- Can become difficult to track and debug with complex event flows.
- May add overhead if too many events are being emitted.
4. URL Parameters/Query Strings
Passing information between micro frontends via URL parameters or query strings is a simple and effective method, especially when you’re working with different routes and micro frontends. This can be a good approach if the communication is related to navigation or the passing of specific states, such as user IDs or page filters.
Example:
Micro Frontend A (passing data via URL):
const navigate = (data) => {
window.location.href = `/other-app?message=${data}`;
};
Micro Frontend B (receiving data from URL):
const params = new URLSearchParams(window.location.search);
const message = params.get('message');
console.log(message); // Output: "Hello from Micro Frontend A"
Advantages:
- Simple to implement and easy to pass information across different parts of the app.
- Works well for route-based communication.
Disadvantages:
- URL length restrictions and encoding concerns.
- Limited in scope, not suitable for complex or frequent communication.
5. Global State Management Libraries
Libraries like Redux, Recoil, or MobX can be used to manage global application state across multiple micro frontends. These libraries provide a centralized store that all micro frontends can access and update.
Example (Using Redux):
// Store.js (Redux Store)
import { createStore } from 'redux';
const initialState = {
message: 'Initial Message',
};
function reducer(state = initialState, action) {
switch (action.type) {
case 'SET_MESSAGE':
return { ...state, message: action.payload };
default:
return state;
}
}
const store = createStore(reducer);
export default store;
In Micro Frontend A, dispatch an action:
// Micro Frontend A (Dispatch Action)
import { useDispatch } from 'react-redux';
import { setMessage } from './store';
const MicroFrontendA = () => {
const dispatch = useDispatch();
const updateMessage = () => {
dispatch(setMessage('Updated by Micro Frontend A'));
};
return <button onClick={updateMessage}>Update Message</button>;
};
In Micro Frontend B, read from the store:
// Micro Frontend B (Read from Redux Store)
import { useSelector } from 'react-redux';
const MicroFrontendB = () => {
const message = useSelector((state) => state.message);
return <div>{message}</div>;
};
Advantages:
- Powerful and scalable, suitable for larger applications.
- Provides a central state management solution.
Disadvantages:
- Can be complex to set up and configure.
- Might introduce additional boilerplate code.
6. API Integration
When micro frontends need to share data or interact with each other in a more decoupled way, you can use API calls. Each micro frontend can expose REST or GraphQL APIs that other micro frontends can call to exchange data.
Example:
Micro Frontend A (Exposing API):
// Express.js server in Micro Frontend A
const express = require('express');
const app = express();
app.get('/api/message', (req, res) => {
res.json({ message: 'Hello from Micro Frontend A' });
});
app.listen(3001);
Micro Frontend B (Calling API):
// Micro Frontend B calling API
fetch('http://localhost:3001/api/message')
.then((response) => response.json())
.then((data) => {
console.log(data.message); // Output: "Hello from Micro Frontend A"
});
Advantages:
- Scalable and works well for decoupled micro frontends.
- Allows for communication across different services or even between frontends and backends.
Disadvantages:
- Requires network requests and can add overhead.
- Needs proper API versioning and error handling.
