Framework Agnostic Communication Patterns for Micro Frontends
Connect React, Vue, Angular (and more) micro-frontends with pure browser APIs—no framework lock-in, zero overhead. Modern micro-frontend architectures often mix diverse technologies on the same page. When a user adds an item to the cart in a React-based Product Catalog, how does a Vue-powered Shopping Cart pick up that change? Traditional, framework-specific solutions simply don’t translate across boundaries. This post explores five core, framework-agnostic communication patterns—plus additional techniques—to glue your micro-frontends together without tying them to any one technology.
1. Window CustomEvents: The Universal Language
CustomEvents are native to every browser and work in any framework.
Publish an event:
const event = new CustomEvent('cart:itemAdded', {
detail: { productId: 'prod-123', name: 'Laptop', price: 999 }
});
window.dispatchEvent(event);
Subscribe to events:
const handler = e => { console.log('Item added:', e.detail); };
window.addEventListener('cart:itemAdded', handler);
// Remove listener on unmount
window.removeEventListener('cart:itemAdded', handler);
Use cases
- E-commerce: Product Catalog → Shopping Cart
- Auth: Broadcast login/logout
- Theme: Apply dark/light modes
- Search: Emit filter or query updates
Learn more: CustomEvent · addEventListener
2. Custom Message Bus: Structured Pub/Sub
For more sophisticated workflows, a shared message bus offers organization and debugging.
// message-bus.js
class MessageBus {
constructor() { this.subs = {}; }
publish(event, data) { (this.subs[event] || []).forEach(h => h(data)); }
subscribe(event, handler) {
this.subs[event] = this.subs[event] || [];
this.subs[event].push(handler);
return () => {
this.subs[event] = this.subs[event].filter(h => h !== handler);
};
}
}
window.messageBus = new MessageBus();
Publish
window.messageBus.publish('user:loggedIn', { userId: 42 });
Subscribe
const unsubscribe = window.messageBus.subscribe('user:loggedIn', data => {
console.log('User logged in:', data);
});
// Call unsubscribe() when done
Use cases
- Sessions: Notify MFEs when users log in/out
- Feature flags: Toggle features at runtime
- Workflows: Orchestrate processes (e.g., checkout)
- Analytics: Centralize event tracking
3. BroadcastChannel: Real-Time Cross-Tab
Keep multiple tabs in sync with the BroadcastChannel API.
const channel = new BroadcastChannel('app_sync');
// Listen
channel.onmessage = ({ data }) => {
if (data.type === 'LOGOUT') window.location.href = '/login';
};
// Send
channel.postMessage({ type: 'LOGOUT' });
// Cleanup
channel.close();
Use cases
- Logout: Close sessions in all tabs
- Cart: Sync across tabs
- Theme: Reflect changes everywhere
- Collaboration: Presence/status updates
Learn more: BroadcastChannel
4. Global Window Object: Simple Shared State
Attach a shared object to window
for straightforward state or utility sharing.
// Initialize in shell
window.__APP_STATE__ = {
user: null,
theme: 'light',
setUser(user) {
this.user = user;
window.dispatchEvent(new CustomEvent('user:changed', { detail: user }));
}
};
Access or update from any micro-frontend
const user = window.__APP_STATE__.user;
window.__APP_STATE__.setUser({ name: 'Alice' });
Use cases
- Config: Global settings or feature flags
- User: Provide context (ID, roles)
- Utils: Common helpers (date formatting)
- Localization: Store locale/translations
5. LocalStorage Events: Persistent Cross-Tab
Combine localStorage
with the storage
event for persistence and cross-tab updates.
// Update cart
localStorage.setItem('cart', JSON.stringify(cart));
// Listen in other tabs
window.addEventListener('storage', e => {
if (e.key === 'cart') {
console.log('Cart updated from another tab:', JSON.parse(e.newValue));
}
});
Use cases
- Cart: Persist and sync
- Drafts: Autosave forms
- Prefs: Store user settings
- Offline: Cache data
Learn more: localStorage · storage
Additional Framework-Agnostic Techniques
- URL Hash Changes: Listen for
hashchange
(MDN) - History API: Use
pushState
/popstate
(MDN) - postMessage: Window messaging (MDN)
- IndexedDB: Complex data storage (MDN)
- SharedWorker: One worker for all contexts (MDN)
- ServiceWorker.postMessage: Worker messaging (MDN)
- MutationObserver: Watch DOM changes (MDN)
- Event Bus Libraries: Pure JS pub/sub (e.g., pubsub-js)
- DOM Attributes/Data Attributes: Shared element data
- Hidden DOM Elements: Off-screen data carriers
Choosing the Right Pattern
Pattern | Same-Page | Cross-Tab | Persistent | Complexity |
---|---|---|---|---|
CustomEvents | ✅ | ❌ | ❌ | Low |
Message Bus | ✅ | ❌ | ❌ | Medium |
BroadcastChannel | ❌ | ✅ | ❌ | Low |
Window Object | ✅ | ❌ | ❌ | Very Low |
LocalStorage | ❌ | ✅ | ✅ | Low |
Final Tips
- Namespace your events (e.g.,
cart:itemAdded
,user:changed
) - Always clean up listeners to prevent memory leaks
- Handle errors gracefully within handlers
- Use TypeScript for event payload definitions
These patterns ensure your micro-frontends remain truly framework-agnostic, flexible, and maintainable across diverse technology stacks.