Micro-Frontends with React: Mastering Module Federation Architecture
Learn to architect scalable micro-frontends with React and Module Federation. Discover how to manage shared dependencies and handle cross-app communication.
Previously in this course, we explored mastering modular directory structures and refactoring for scalability to organize monolithic codebases. While those techniques keep a single repo clean, they don't solve the "deployment bottleneck" where a single team's error can block the entire organization. Today, we shift toward Micro-frontends, an architecture that allows teams to develop, deploy, and scale application slices independently.
The Architectural Shift: Why Micro-frontends?
Micro-frontends extend the concept of microservices to the browser. Instead of one massive bundle, you partition the UI into separate, independently deployable applications that compose at runtime.
The most robust way to achieve this today is via Module Federation, a feature introduced in Webpack 5. Unlike older iframe-based approaches—which suffer from poor accessibility and limited state sharing—Module Federation allows applications to dynamically load code from other builds at runtime as if it were a local dependency.
Core Concepts of Module Federation
- Host: The container app that loads remote modules.
- Remote: The micro-frontend exposing modules (components, utilities, or logic).
- Shared Dependencies: Configuration to prevent shipping multiple copies of React (or other libraries) to the user's browser.
Worked Example: Implementing a Remote Component
Let's assume our running project requires a "Header" micro-frontend that can be updated independently of the main "Dashboard" application.
1. Configuring the Remote (Header App)
In the Header app's webpack.config.js, we expose the component:
JAVASCRIPTconst { ModuleFederationPlugin } = require(CE9178">'webpack').container; module.exports = { plugins: [ new ModuleFederationPlugin({ name: CE9178">'headerApp', filename: CE9178">'remoteEntry.js', // The manifest file exposes: { CE9178">'./Header': CE9178">'./src/components/Header', }, shared: { react: { singleton: true }, CE9178">'react-dom': { singleton: true } }, }), ], };
2. Consuming in the Host (Dashboard App)
In the Dashboard app, we map the remote:
JAVASCRIPT// webpack.config.js new ModuleFederationPlugin({ name: CE9178">'dashboardApp', remotes: { headerApp: CE9178">'headerApp@http://localhost:3001/remoteEntry.js', }, shared: { react: { singleton: true }, CE9178">'react-dom': { singleton: true } }, });
3. Dynamic Loading
We use React.lazy to import the remote component, wrapped in Suspense because the remote is fetched over the network:
JSXimport React, { Suspense } from CE9178">'react'; const RemoteHeader = React.lazy(() => import(CE9178">'headerApp/Header')); function App() { return ( <div> <Suspense fallback={<div>Loading Header...</div>}> <RemoteHeader /> </Suspense> <main>Dashboard Content</main> </div> ); }
Managing Shared Dependencies and Communication
A common pitfall is version mismatch. By setting singleton: true in your shared configuration, you force Webpack to use the highest version of a library found in the dependency tree across all federated apps, preventing the "multiple React instances" bug which breaks hooks.
For cross-app communication, avoid tight coupling via shared state stores like Redux. Instead, use:
- Custom Events: The browser's native
window.dispatchEventfor loose coupling. - Props/Callbacks: Passing functions from the host to the remote via component props.
- URL Parameters/Query Strings: For persistent state that should survive reloads.
Comparison: Integration Strategies
| Strategy | Coupling | Deployment | Performance |
|---|---|---|---|
| Monolith | Tight | Atomic | Best (no network hops) |
| iFrames | Loose | Independent | Poor (heavy footprint) |
| Module Federation | Loose | Independent | Excellent (lazy loading) |
Hands-on Exercise
- Setup: Create two separate React projects using Vite or Webpack.
- Expose: In Project A, expose a
UserWidgetcomponent. - Consume: In Project B, dynamically import
UserWidget. - Verify: Open the Network tab in Chrome DevTools. You should see
remoteEntry.jsbeing fetched when the page loads, followed by the specific chunk for the widget. - Challenge: Pass a
userNameprop from the Host to the Remote and verify it renders correctly.
Common Pitfalls
- Version Mismatch: If you don't share React as a singleton, you will encounter the dreaded "Hooks can only be called inside the body of a function component" error because the remote will try to use its own version of React.
- CSS Leaks: Styles in micro-frontends can bleed. Use CSS Modules or Styled Components with unique namespaces to ensure your Header's styles don't conflict with the Dashboard's.
- Over-federating: Don't turn every component into a micro-frontend. This adds significant network complexity. Only decouple boundaries that represent distinct business domains or team responsibilities.
Recap
Micro-frontends using Module Federation provide the architectural scalability required for enterprise applications. By focusing on independent deployments, shared dependency management, and loose event-based communication, you can maintain a high-velocity development cycle without sacrificing stability.
Up next: We will secure our distributed architecture in Security Best Practices in React, covering how to protect sensitive data flow across micro-frontend boundaries.
Work with me

Headless WordPress + Next.js Frontend Development
Keep WordPress for content, get a lightning-fast Next.js frontend. The best of both worlds — familiar editing, modern speed.

React & Next.js Dashboard / Admin UI Development
A clean, data-rich dashboard UI in React or Next.js — charts, tables, and real-time data that your users will actually enjoy using.