Micro-frontends with Module Federation: A ‘Divide and Conquer’ Solution for Massive React Apps

Development tutorial - IT technology blog
Development tutorial - IT technology blog

The Nightmare Named Monolith Frontend

I once led a React project with a codebase exceeding 50,000 lines. It was a total nightmare. Just changing a single line of CSS in the Header meant waiting 20 minutes for Jenkins to build. Even worse, a tiny logic bug on the Profile page could crash the entire Checkout page. That’s when I realized the Monolithic architecture had reached its limit as the team scaled.

Micro-frontends emerged as a lifesaver, much like how Microservices changed the game for Backend development. Instead of wrestling with a massive monolithic codebase, I split it into independently running modules. Each team can now freely develop, test, and deploy their own parts without having to worry about other teams’ schedules.

Three Common Ways to Implement Micro-frontends

Before diving into the code, you need to choose the right tool. There is no silver bullet; only the solution that best fits your current problem.

1. iFrames: The Aging Predecessor

This is the simplest approach. Each micro-app is a standalone website contained within an <iframe> tag. While it provides excellent fault isolation, it’s a disaster for performance and SEO. Sharing data between apps via postMessage is also extremely cumbersome.

2. NPM Packages: Secure but Slow

You package modules as libraries and install them into the main app. This method offers strict version management. However, every time Team A updates a button, Team B (the Host app) must reinstall and rebuild everything. It doesn’t solve the independent deployment problem we’re looking for.

3. Module Federation: The Revolution from Webpack 5

This is the “holy grail” for modern projects. Module Federation allows an application to dynamically load code from another server at runtime. The Host app doesn’t need to rebuild when a Remote app changes. In my most recent project, I reduced overall build time by 85% using this method.

The Price of Flexibility: Pros and Cons

Powerful as it is, Module Federation isn’t a magic wand. After hitting a few roadblocks in production, I’ve noted a few key points:

  • Pros:
    • Resource Optimization: If both apps use React, the browser only loads a single instance.
    • Absolute Autonomy: The Dashboard team can deploy 10 times a day without affecting the Profile team.
    • Ultra-lean Bundle Size: Instead of forcing users to download a 5MB bundle, they only download 200KB for each module they actually use.
  • Cons:
    • Configuration Complexity: A single misplaced bracket in the Webpack config can lead to a white screen of death.
    • CSS Conflicts: Without CSS Modules or Tailwind, classes from one app can easily override another.
    • State Management: Syncing user info across apps requires an Event Bus strategy or a thin Global State.

Getting Hands-on: React + Webpack 5

Suppose we have two apps: Host (main page) and Remote (product widget). My hard-earned experience: write thorough Unit Tests before splitting modules. Otherwise, bugs will jump between apps and become very difficult to trace.

Step 1: Configuring the Remote App

In remote-app/webpack.config.js, I use ModuleFederationPlugin to expose the component.

const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
const deps = require("./package.json").dependencies;

module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: "remoteApp",
      filename: "remoteEntry.js",
      exposes: {
        "./ProductWidget": "./src/components/ProductWidget",
      },
      shared: {
        ...deps,
        react: { singleton: true, requiredVersion: deps.react },
        "react-dom": { singleton: true, requiredVersion: deps["react-dom"] },
      },
    }),
  ],
};

Note: singleton: true is mandatory to ensure only a single React instance runs in the background, avoiding Hook conflict errors.

Step 2: Configuring the Host App

In host-app/webpack.config.js, I register to receive code from the Remote.

new ModuleFederationPlugin({
  name: "hostApp",
  remotes: {
    remoteApp: "remoteApp@http://localhost:3001/remoteEntry.js",
  },
  shared: {
    ...deps,
    react: { singleton: true, requiredVersion: deps.react },
    "react-dom": { singleton: true, requiredVersion: deps["react-dom"] },
  },
}),

Step 3: Embedding the Component into the UI

Since the code is loaded over the network, I must use React.lazy so the application doesn’t hang while waiting for the download.

import React, { Suspense } from "react";
const ProductWidget = React.lazy(() => import("remoteApp/ProductWidget"));

const App = () => (
  <div>
    <h1>Main Interface</h1>
    <Suspense fallback={<p>Connecting to product module...</p>}>
      <ProductWidget />
    </Suspense>
  </div>
);

Real-world Lessons for Production

Getting it to run on localhost is only 50% of the journey. When moving to a real environment, pay attention to:

  1. Dynamic URLs: Don’t hardcode localhost URLs. Use environment variables to point to the correct Staging or Production domains.
  2. Error Boundaries: Always wrap Remote components in an Error Boundary. If the server hosting the product module goes down, your main website should still display the rest of the content instead of crashing completely.
  3. Versioning: Have a versioning strategy for remoteEntry.js to avoid browser caching issues when you update new code.

Switching to Micro-frontends isn’t just a technology change; it’s a shift in team mindset. If your project is bloating and teams are starting to step on each other’s toes, Module Federation is currently the most effective way out.

Share: