Sharing state between micro frontends is a crucial aspect of building modular, independent applications that still need to interact and share data. Since micro frontends are intended to be independently deployed and developed, ensuring they can share state without tightly coupling them is key to maintaining their autonomy. Below, we’ll explore various strategies for effectively sharing state across micro frontends while keeping them loosely coupled.
Approaches for Sharing State Between Micro Frontends
- Custom Global Event Bus (Pub/Sub Pattern)
- Shared Global Store (e.g., Redux, React Context)
- Browser Storage (LocalStorage/SessionStorage)
- Custom API/Backend Communication
- URL Parameters / Query Strings
- Cross-Domain State Sharing (PostMessage API)
1. Custom Global Event Bus (Pub/Sub Pattern)
A Pub/Sub (Publish/Subscribe) pattern is an effective way to share state between micro frontends without them directly communicating with each other. In this pattern, micro frontends can “publish” events when their state changes, and other micro frontends can “subscribe” to those events and react to them.
This allows micro frontends to remain decoupled while still responding to shared events or state changes.
Example Using a Simple Event Bus:
// EventBus.js
class EventBus {
constructor() {
this.events = {};
}
subscribe(event, handler) {
if (!this.events[event]) {
this.events[event] = [];
}
this.events[event].push(handler);
}
publish(event, data) {
if (this.events[event]) {
this.events[event].forEach((handler) => handler(data));
}
}
}
const eventBus = new EventBus();
export default eventBus;
Publisher Micro Frontend (Frontend A):
// Micro Frontend A publishes an event when a state changes
import eventBus from './EventBus';
const updateState = (newState) => {
eventBus.publish('stateUpdated', newState);
};
Subscriber Micro Frontend (Frontend B):
// Micro Frontend B subscribes to the event and reacts to state changes
import eventBus from './EventBus';
eventBus.subscribe('stateUpdated', (newState) => {
console.log('Received updated state:', newState);
});
Advantages:
- Decouples micro frontends while allowing communication.
- Easy to implement using custom or third-party libraries.
- Works well for event-driven architectures.
Disadvantages:
- Can become hard to maintain if there are too many events.
- Not ideal for complex, shared state management.
2. Shared Global Store (e.g., Redux, React Context)
A shared global store is a common approach when micro frontends need to access and update a shared state. Using a state management solution like Redux or React Context, you can create a shared store that all micro frontends can read from and update, making the state available across all parts of the application.
This method works particularly well in applications where multiple micro frontends need to work with the same data.
Example with Redux:
// store.js (Redux Store)
import { createStore } from 'redux';
// Reducer to manage state
const reducer = (state = { message: 'Initial state' }, action) => {
switch (action.type) {
case 'SET_MESSAGE':
return { ...state, message: action.payload };
default:
return state;
}
};
const store = createStore(reducer);
export default store;
Micro Frontend A (Dispatching Action):
// Micro Frontend A dispatches an action to update state
import { useDispatch } from 'react-redux';
const MicroFrontendA = () => {
const dispatch = useDispatch();
const setMessage = () => {
dispatch({ type: 'SET_MESSAGE', payload: 'Updated from Micro Frontend A' });
};
return <button onClick={setMessage}>Update State</button>;
};
export default MicroFrontendA;
Micro Frontend B (Reading State):
// Micro Frontend B reads from the shared Redux store
import { useSelector } from 'react-redux';
const MicroFrontendB = () => {
const message = useSelector((state) => state.message);
return <div>{message}</div>;
};
export default MicroFrontendB;
Advantages:
- Provides a consistent and centralized state management solution.
- Works well for React-based micro frontends.
- Scalable and easy to maintain.
Disadvantages:
- Requires a common state management solution (like Redux or Context).
- Adds complexity to manage large, shared state.
3. Browser Storage (LocalStorage/SessionStorage)
For lightweight state sharing between micro frontends, you can use browser storage such as LocalStorage or SessionStorage. These storage options are shared across different windows or tabs of the same domain, allowing micro frontends to access and update the stored state.
While this method is simple to implement, it’s suitable for less complex or transient state, such as user preferences or session data.
Example:
Micro Frontend A (Writing to LocalStorage):
// Micro Frontend A writes data to LocalStorage
localStorage.setItem('message', 'Hello from Micro Frontend A');
Micro Frontend B (Reading from LocalStorage):
// Micro Frontend B reads data from LocalStorage
const message = localStorage.getItem('message');
console.log(message); // Output: "Hello from Micro Frontend A"
Advantages:
- Easy to implement with minimal overhead.
- No external libraries are needed.
Disadvantages:
- Limited to simple data (strings and numbers).
- Not suitable for complex or large data sharing.
- The data is not reactive (does not automatically update across micro frontends without polling).
4. Custom API/Backend Communication
Micro frontends can share state via API calls to a shared backend. This approach ensures that the state is consistent across all micro frontends, as they fetch data from a centralized service.
This method is ideal when state changes need to be persistent and shared across different users or sessions.
Example:
Micro Frontend A (Calling API to Save State):
// Micro Frontend A makes an API call to save state
fetch('https://api.example.com/update-state', {
method: 'POST',
body: JSON.stringify({ message: 'Updated from Micro Frontend A' }),
headers: { 'Content-Type': 'application/json' },
});
Micro Frontend B (Fetching State via API):
// Micro Frontend B calls API to get the latest state
fetch('https://api.example.com/get-state')
.then((response) => response.json())
.then((data) => {
console.log(data.message); // Output: "Updated from Micro Frontend A"
});
Advantages:
- Ensures consistency across all micro frontends.
- Works well for scenarios requiring persistent state (e.g., user data, app configuration).
Disadvantages:
- Adds network latency and requires backend setup.
- Might involve complex API versioning.
5. URL Parameters / Query Strings
For certain state-sharing scenarios, you can pass state via URL parameters or query strings. This is particularly useful when navigating between micro frontends, such as sharing filtering criteria or user selections.
Example:
Micro Frontend A (Setting URL Parameters):
// Update the URL to include state as a query parameter
const navigateToB = (message) => {
window.location.href = `/micro-frontend-b?message=${encodeURIComponent(message)}`;
};
Micro Frontend B (Reading from URL Parameters):
// Read query string in Micro Frontend B
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, especially for navigation-based state.
- Works well when transitioning between different views or pages.
Disadvantages:
- Limited to small amounts of data.
- URL length limits and encoding concerns.
6. Cross-Domain State Sharing (PostMessage API)
When micro frontends are hosted on different domains or subdomains, PostMessage API can be used for cross-origin communication. This API allows different windows, frames, or iframes to send messages to each other securely, enabling state sharing between micro frontends.
Example:
Micro Frontend A (Sending Message via PostMessage):
// Send message to another window
window.postMessage({ type: 'stateUpdate', message: 'Hello from Micro Frontend A' }, '*');
Micro Frontend B (Receiving Message via PostMessage):
// Listen for messages from other micro frontends
window.addEventListener('message', (event) => {
if (event.data.type === 'stateUpdate') {
console.log(event.data.message); // Output: "Hello from Micro Frontend A"
}
});
Advantages:
- Allows cross-domain communication.
- Decouples micro frontends and enables state sharing even across different applications.
Disadvantages:
- Requires careful handling of security and message validation (to avoid cross-site scripting attacks).
- Adds complexity in managing cross-origin communication.