Microfrontend Communication Patterns
Three ways microfrontends talk to each other — Nanostores, Broadcast Channel, and Comlink — and how to pick the right one.
Once you've accepted that the Context API doesn't reach across React trees, the next question is: what does? Separate remotes still run in the same browser tab, which gives you more options than you might think.
Here are the three I'd reach for, in order of preference.
Nanostores: Framework-Agnostic Shared State
Nanostores is under 1 kilobyte. You could probably write it yourself — I've done it before, not because I'm particularly clever but because I didn't know the library existed at the time.
The primitive is an atom: a small container for a value that announces changes through a subscribe mechanism.
import { atom } from 'nanostores';
// Create an atom with an initial value
export const authUser = atom<User | null>(null);
// Read the current value
const user = authUser.get(); // null
// Update it
authUser.set({ id: '1', name: 'Grace Hopper', role: 'admin' });
// Subscribe to changes
const unsubscribe = authUser.subscribe((user) => {
console.log('Auth state changed:', user);
});The subscribe mechanism is what makes this useful for federated systems. You import the same atom in both the shell and each remote. When the shell updates auth state, every remote that's subscribed fires its callback — even though they're in separate React trees.
Using It With React
Nanostores provides a useStore hook for each framework:
// @nanostores/react
import { useStore } from '@nanostores/react';
import { authUser } from '../shared/stores';
function UserAvatar() {
const user = useStore(authUser);
if (!user) return <LoginButton />;
return <Avatar name={user.name} />;
}Under the hood, useStore is roughly useState + useEffect with a subscription and cleanup — about 10 lines of code. The key thing is that it's reactive: when the atom changes anywhere in the app, every component using useStore re-renders with the new value.
For Nested Objects: deepMap
For more complex shared state, deepMap lets you update individual keys without creating a new top-level object:
import { deepMap } from 'nanostores';
export const appState = deepMap({
theme: 'dark',
locale: 'en',
sidebar: { collapsed: false },
});
// Update a specific key
appState.setKey('theme', 'light');
// Update a nested key
appState.setKey('sidebar.collapsed', true);This is useful for global app settings — theme, locale, user preferences — that multiple teams' remotes need to read and react to.
Why It Works for Federated Systems
The atom is just a JavaScript object with a subscribe method. It can be:
- Imported by the shell and any number of remotes from a shared package
- Used in React, Vue, Svelte, Angular, or vanilla JS — there are adapters for all of them
- Svelte doesn't even need an adapter: anything with a
subscribemethod works as a store natively
This framework-agnosticism is why I reach for it in federated systems. If you're migrating from one framework to another across teams — a common reason to use microfrontends — your shared state can stay the same while the framework around it changes.
Broadcast Channel: Cross-Tab Messaging
Broadcast Channel is a browser API, no library required:
const channel = new BroadcastChannel('app-events');
// Sender
channel.postMessage({ type: 'USER_LOGGED_OUT' });
// Receiver (any browsing context on the same origin)
channel.onmessage = (event) => {
if (event.data.type === 'USER_LOGGED_OUT') {
clearLocalCache();
redirectToLogin();
}
};The key property: every browsing context on the same origin receives the message — tabs, iframes, web workers. This makes it useful for coordinating across tabs, not just across remotes in the same tab.
The limitation: messages are essentially fire-and-forget strings (or JSON-serializable values). No type safety without manual enforcement, no request/response pattern, no history of previous messages. It's a good fit for one-way event notifications — "user logged out," "theme changed," "session expired" — not for complex state synchronization.
I've used it mainly for triggering refreshes and session coordination across tabs, not as the primary state sharing mechanism.
Comlink: Ergonomic Worker Communication
Comlink wraps the postMessage API for web workers, service workers, and other browser contexts with an interface that feels like regular function calls:
// worker.ts
import { expose } from 'comlink';
const api = {
async computeHeavyThing(input: number) {
return input * 2; // imagine this is actually expensive
},
};
expose(api);
// main.ts
import { wrap } from 'comlink';
const worker = new Worker('./worker.ts');
const api = wrap<typeof api>(worker);
const result = await api.computeHeavyThing(42); // 84For microfrontend communication specifically, Comlink is useful when you're bridging contexts that communicate via postMessage — iframes, for instance, or web workers running shared computation.
For the common case of sharing state between remotes in the same window, Nanostores is simpler. Comlink becomes relevant when you're working with the more isolated patterns (iframes, service workers) or when you want a request/response style communication pattern.
Putting It Together
For most federated systems, the approach I'd recommend:
- Nanostores for shared state — auth, user preferences, theme, anything that needs to be reactive across remotes
- Broadcast Channel for event notifications — session expiry, cross-tab coordination, one-way events where no response is needed
- Comlink when you need it — iframe communication, worker communication, or request/response patterns
The underlying principle: these remotes share a browser window, even when they're separate deployments. The browser's own primitives — JavaScript objects, custom events, Broadcast Channel — are available to all of them. The key is picking the right level of abstraction for what you're trying to coordinate.
None of this is particularly hard to implement. The complexity is in recognizing that the problem exists — that sharing context state across React trees doesn't work out of the box — and putting the coordination layer in place before you need it.
Keep reading
Enjoyed this? Get more like it.
Deep dives on system design, React, web development, and personal finance — straight to your inbox. Free, always.