Back to Blog
Lesson 42 of the Advanced React: Performance, Architecture & Patterns course
ReactJune 28, 20264 min read

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.

ReactArchitectureMicro-frontendsModule FederationScalabilityWebpackjavascriptfrontend

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:

JAVASCRIPT
const { 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:

JSX
import 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:

  1. Custom Events: The browser's native window.dispatchEvent for loose coupling.
  2. Props/Callbacks: Passing functions from the host to the remote via component props.
  3. URL Parameters/Query Strings: For persistent state that should survive reloads.

Comparison: Integration Strategies

StrategyCouplingDeploymentPerformance
MonolithTightAtomicBest (no network hops)
iFramesLooseIndependentPoor (heavy footprint)
Module FederationLooseIndependentExcellent (lazy loading)

Hands-on Exercise

  1. Setup: Create two separate React projects using Vite or Webpack.
  2. Expose: In Project A, expose a UserWidget component.
  3. Consume: In Project B, dynamically import UserWidget.
  4. Verify: Open the Network tab in Chrome DevTools. You should see remoteEntry.js being fetched when the page loads, followed by the specific chunk for the widget.
  5. Challenge: Pass a userName prop 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.

Similar Posts