How do you manage state across micro-frontends in JavaScript?
JavaScript Developer
answer
I design micro-frontend state using clear ownership boundaries, reactive streams (RxJS/Observables), and shared stores via Module Federation or custom event bridges. I isolate long-running computations in web workers and clean subscriptions on unmount to prevent memory leaks. Synchronization uses versioned events and debounced updates to avoid race conditions. Communication is unidirectional or event-driven, with explicit cleanup routines and throttled message channels.
Long Answer
Managing state and data flow across JavaScript micro-frontends requires explicit boundaries, reactive design, and careful lifecycle management. My approach ensures consistency, performance, and avoids memory leaks or race conditions.
1) Ownership boundaries
Each micro-frontend owns its local state and exposes only controlled interfaces for sharing. Shared state is minimized and exposed via Module Federation, global observables, or well-defined event emitters. This reduces coupling and prevents accidental cross-module mutations.
2) Reactive streams with RxJS/Observables
I employ RxJS observables to propagate events across modules. Observables are memory-managed with takeUntil, unsubscribe, or Subscription arrays to ensure cleanup on component unmount. State updates are emitted as immutable snapshots or deltas, reducing side effects and making race conditions predictable.
3) Module Federation and shared stores
Module Federation allows dynamic loading of remote micro-frontends and optionally shared stores. I synchronize shared state with a single source of truth using immutable patterns or BehaviorSubjects. Versioned events prevent older micro-frontends from overriding newer state, ensuring consistent snapshots.
4) Custom events and event buses
For loosely-coupled communication, I use custom events or lightweight event buses. Events carry metadata (source ID, timestamp, version) to enforce ordering and prevent stale updates. Event listeners are registered dynamically and removed on unmount to prevent dangling references or memory leaks.
5) Web workers for heavy computations
Long-running computations, aggregation, or transformations are offloaded to web workers. Workers communicate via structured messages and observables, preventing UI blocking and minimizing memory footprint in the main thread. Workers are terminated on module unload or route change to free resources.
6) Debouncing, throttling, and synchronization
State updates from multiple micro-frontends are coordinated using debounce or throttle operators in RxJS to avoid floods of updates that could cause race conditions. Critical cross-module writes may use versioned transactions or locks in a central store to enforce atomicity.
7) Immutable state and snapshots
Shared state is immutable; updates create new objects or patches. Modules subscribe to the latest snapshot rather than raw references. This prevents accidental mutation, facilitates undo/redo, and simplifies comparison for conditional rendering or event processing.
8) Cleanup and lifecycle management
Subscriptions, event listeners, and worker instances are cleaned up on unmount using Angular’s ngOnDestroy, React useEffect cleanup, or framework-neutral hooks. Any timers, intervals, or DOM observers are similarly disposed to prevent leaks.
9) Cross-module testing
I test micro-frontend integration with simulated event storms, race conditions, and unmount/remount scenarios. Memory profiling ensures no retained subscriptions or worker leaks. Integration tests validate that shared stores remain consistent even under high-frequency updates.
10) Error handling and resilience
Errors in one micro-frontend are contained and do not propagate unintended state changes. RxJS error operators (catchError, retryWhen) and worker message error handlers provide controlled fallback. Modules can unsubscribe and recover gracefully.
By combining reactive streams, strict ownership boundaries, Module Federation, custom event metadata, immutable snapshots, and explicit cleanup routines, JavaScript micro-frontends can share state efficiently while avoiding memory leaks, race conditions, or stale updates.
Table
Common Mistakes
Sharing mutable objects across micro-frontends, causing stale or inconsistent state. Forgetting to unsubscribe from observables or remove event listeners, producing memory leaks. Updating shared stores directly without versioning, causing race conditions. Running heavy computations on the main thread, blocking the UI. Using global variables for state instead of proper module federation or reactive patterns. Failing to terminate web workers on unmount. Ignoring testing for unmount/remount, event storms, or cross-module updates. Not handling errors in workers or observables, leading to silent failures.
Sample Answers
Junior:
“I use local state in each micro-frontend and share data via custom events. RxJS observables propagate changes, and I unsubscribe on unmount to prevent leaks. Heavy calculations run in web workers. Updates are debounced to avoid race conditions.”
Mid-level:
“I implement shared stores via Module Federation with BehaviorSubjects. Observables propagate updates, and all subscriptions are cleaned up on component unmount. Events carry timestamps and source metadata to maintain ordering. Web workers handle expensive tasks, and updates are merged immutably. Debouncing and throttling avoid race conditions.”
Senior:
“I design micro-frontends with strict state ownership and reactive streams. Shared state uses versioned BehaviorSubjects via Module Federation. Cross-module events include timestamps, source IDs, and versioning to enforce ordering. Web workers offload heavy computations and are terminated on unmount. Subscriptions, listeners, timers, and intervals are cleaned proactively. Immutable snapshots prevent accidental mutation. Debounce/throttle operators coordinate high-frequency updates, and RxJS error handling ensures resilience.”
Evaluation Criteria
Look for clear ownership of state per micro-frontend, reactive propagation via RxJS or observables, and safe shared stores through Module Federation. Strong answers include immutable snapshots, event metadata (source, timestamp, version), debouncing/throttling, web worker offloading, and explicit cleanup of subscriptions/listeners. Error handling and testing for unmount, race conditions, and high-frequency events are expected. Red flags: mutable shared state, dangling subscriptions, global variables, main-thread heavy computation, and no race-condition mitigation.
Preparation Tips
- Define clear state ownership per micro-frontend; share minimal state.
- Use RxJS observables or reactive stores for cross-module communication.
- Apply Module Federation for shared modules or stores with versioning.
- Propagate events with metadata (source ID, timestamp, version) to maintain ordering.
- Offload heavy computations to web workers and terminate on unmount.
- Implement immutable updates; avoid direct object mutations.
- Clean subscriptions, event listeners, timers, and observers on unmount.
- Debounce or throttle frequent events to prevent race conditions.
- Test integration with high-frequency events, unmount/remount, and memory profiling.
- Handle errors in observables and workers gracefully, ensuring module isolation.
Real-world Context
A large-scale dashboard used micro-frontends for charts, tables, and forms. Each module owned its state, while shared metrics were exposed via a BehaviorSubject using Module Federation. Cross-module events carried timestamps and source IDs to enforce ordering. Web workers calculated heavy aggregations off the main thread. All subscriptions and listeners were cleaned on unmount. Debounce and throttle operators prevented update storms. Integration tests simulated multiple simultaneous updates and unmount/remount scenarios. Result: consistent state across modules, zero memory leaks, and no race conditions under heavy load.
Key Takeaways
- Define ownership boundaries per micro-frontend and expose only controlled interfaces.
- Use RxJS/observables for reactive state propagation with cleanup on unmount.
- Share state via Module Federation with versioning and immutable snapshots.
- Employ custom events with source IDs, timestamps, and versioning to prevent race conditions.
- Offload heavy computations to web workers and terminate them on module unload.
- Debounce or throttle frequent updates to synchronize state safely.
- Test unmount/remount scenarios, race conditions, and memory usage proactively.
- Handle errors in workers and observables to isolate failures and maintain module integrity.
Practice Exercise
Scenario:
You are building a micro-frontend dashboard with a charts module, a table module, and a forms module. Users can update filters, submit edits, and trigger live calculations while multiple modules are loaded. Updates must propagate consistently, avoid memory leaks, and handle high-frequency events.
Tasks:
- Define ownership boundaries: each module manages its local state and exposes controlled observables.
- Implement shared state using Module Federation with BehaviorSubjects, exposing only snapshots or immutable deltas.
- Propagate cross-module events with timestamps, source IDs, and version numbers.
- Offload heavy computations to web workers and terminate them on module unmount.
- Use RxJS operators (takeUntil, unsubscribe, debounceTime, throttleTime) to manage updates safely.
- Ensure immutable updates for shared state to prevent accidental mutation.
- Clean subscriptions, event listeners, and timers on unmount to avoid memory leaks.
- Simulate high-frequency updates, concurrent edits, and unmount/remount to verify no race conditions or stale state.
- Handle errors gracefully in observables and worker messages, logging failures with correlation IDs.
- Document the architecture and provide a runbook for scaling to additional micro-frontends.
Deliverable:
A JavaScript micro-frontend implementation demonstrating reactive, immutable state, safe cross-module communication, background worker computations, memory leak prevention, and coordinated updates under heavy load.

