Welcome back, intrepid architect! In the previous chapters, we laid the groundwork for building robust React applications, understanding rendering strategies, and scaling our components. But what happens when your application grows so large that a single team can no longer manage it effectively, or when different parts of your UI need to be developed and deployed completely independently?

This is where Microfrontends come into play. Just as microservices revolutionized backend development by breaking down monolithic applications into smaller, manageable services, microfrontends apply a similar philosophy to the frontend. In this chapter, we’ll dive deep into this powerful architectural pattern, focusing on how Webpack Module Federation enables us to build truly decoupled and scalable React user interfaces.

You’ll learn:

  • What microfrontends are, their benefits, and their challenges.
  • How Webpack Module Federation works under the hood to enable runtime module sharing.
  • Practical steps to build a host application that consumes multiple independent microfrontend “remotes.”
  • Strategies for managing shared dependencies and avoiding common pitfalls.

Before we begin, a solid understanding of React component lifecycles, Webpack fundamentals (from Chapter 2), and basic routing concepts will be helpful. Ready to deconstruct and reconstruct your frontend like never before? Let’s get started!


What are Microfrontends? A Frontend Microservices Approach

Imagine a massive, sprawling enterprise application โ€“ a dashboard with many widgets, a complex user profile section, an administration panel, and several other distinct features. If all these features are built within a single React application, often called a “monolith,” you might encounter several issues:

  • Slow Development Cycles: A single codebase means more coordination, longer build times, and increased risk for every deployment.
  • Team Bottlenecks: Multiple teams working on the same codebase can lead to merge conflicts, dependency hell, and stepping on each other’s toes.
  • Technology Lock-in: Choosing a specific React version or library for the entire monolith makes it hard to experiment with newer technologies for individual features.
  • Deployment Risks: A bug in one small feature can potentially bring down the entire application.

Microfrontends offer a solution by breaking down the monolithic frontend into smaller, independently deployable applications. Each microfrontend represents a distinct business domain or feature, owned by a small, autonomous team.

Think of it like building a house:

  • Monolith: One large construction crew builds the entire house from foundation to roof, all at once. If the plumbing team is delayed, the whole house is delayed.
  • Microfrontends: Different specialist teams build specific rooms (kitchen, living room, bedroom) independently. The kitchen team can finish and move on while the living room team is still working. Finally, everything is integrated into a cohesive home.

Benefits of Microfrontends:

  • Independent Development & Deployment: Teams can build, test, and deploy their microfrontends without affecting or waiting for other teams. This accelerates delivery.
  • Team Autonomy: Each team can choose its own tech stack (within reasonable boundaries), allowing for innovation and better ownership.
  • Scalability: Easier to scale teams and features independently.
  • Resilience: A failure in one microfrontend might not bring down the entire application.
  • Incremental Upgrades: Easier to upgrade specific parts of the application or even rewrite small sections without a full-scale migration.

Challenges of Microfrontends:

  • Increased Operational Complexity: More repositories, more build pipelines, more deployments to manage.
  • Shared State Management: How do different microfrontends communicate or share data (e.g., user authentication status)?
  • Consistent User Experience (UX): Ensuring a unified look and feel, shared design systems, and seamless navigation across disparate applications.
  • Performance Overhead: Careful management of shared dependencies is crucial to avoid downloading the same libraries multiple times.

Webpack Module Federation: The Game Changer

In the past, implementing microfrontends often involved complex iframe setups, client-side composition, or server-side composition. While these methods worked, they often came with significant drawbacks in terms of performance, development experience, or complexity.

Enter Webpack Module Federation, introduced in Webpack 5. This powerful feature provides a native, robust, and elegant solution for building microfrontends. It allows multiple Webpack builds to expose and consume JavaScript modules from each other at runtime.

How it Works (The Mental Model):

Imagine you have two separate React applications, App A and App B. App A wants to use a component from App B. With Module Federation:

  1. App B is configured to “expose” its component. It becomes a Remote application.
  2. App A is configured to “consume” that component. It becomes a Host application.
  3. At runtime, when App A needs the component from App B, Webpack handles loading App B’s exposed module.

This loading happens dynamically, meaning the host application doesn’t need to know about the remote’s code at build time. It simply knows where to find it.

Let’s visualize this interaction:

graph TD User[User s Browser] --> HostApp[Host Application] HostApp --->|Loads Remote Entry| Remote1[Remote 1] HostApp --->|Loads Remote Entry| Remote2[Remote 2] subgraph Host_Application["App Shell"] H1[Webpack Config remotes] H2[Loads Remote Components Dynamically] H3[Orchestrates UI] end subgraph Remote_1["Dashboard Widget"] R1A[Webpack Config exposes] R1B[Exposes DashboardWidget Component] R1C[Independent Build Deployment] end subgraph Remote_2["User Profile"] R2A[Webpack Config exposes] R2B[Exposes UserProfile Component] R2C[Independent Build Deployment] end HostApp --> H1 HostApp --> H2 HostApp --> H3 Remote1 --> R1A Remote1 --> R1B Remote1 --> R1C Remote2 --> R2A Remote2 --> R2B Remote2 --> R2C

Key Concepts in Module Federation:

  • ModuleFederationPlugin: The core Webpack plugin that enables this functionality.
  • name: A unique name for your application (remote or host) to be referenced by others.
  • filename: The name of the remote entry file (e.g., remoteEntry.js) that will be loaded by consuming applications.
  • exposes: An object mapping a local module path to an exposed module name. This is how a remote application makes its components available.
  • remotes: An object mapping a desired remote name to its remote entry URL. This is how a host application defines which remotes it can consume.
  • shared: A crucial configuration for defining dependencies that should be shared between applications. This prevents multiple copies of libraries like React from being downloaded, which is vital for performance.

A Real Production Failure Story: The Monolith’s Demise

Consider “GlobalCorp,” a fictional company with a massive internal analytics dashboard. Initially, a single team built it as a React monolith. As the company grew, multiple product teams needed to add their specific analytics widgets.

The Problem: Every new widget, every bug fix, and every dependency update required a full build and deployment of the entire monolithic dashboard.

  • Deployment Freeze: When Team Alpha needed to deploy a critical security patch, Team Beta’s new feature was held hostage, waiting for Alpha’s release train.
  • Dependency Collisions: Team Gamma updated a common UI library, inadvertently introducing a breaking change for Team Delta’s widget, causing a P0 production outage that took down the entire dashboard.
  • Slow Builds: The CI/CD pipeline for the monolith took over an hour, leading to developer frustration and slow feedback loops.
  • Fear of Change: The codebase became so large and intertwined that engineers were hesitant to refactor or upgrade core libraries, fearing unknown side effects.

This led to missed deadlines, increased operational costs, and a significant drop in developer morale. The solution? A strategic shift to microfrontends using Webpack Module Federation, allowing each product team to own, build, and deploy their widgets independently, transforming the monolithic beast into a flexible, scalable ecosystem.


Step-by-Step Implementation: Building a Microfrontend Enterprise Suite

Let’s build a simple microfrontend suite. We’ll create:

  1. An app-shell (host application) that provides the main layout and navigation.
  2. A dashboard-widget (remote application) that exposes a simple data display widget.
  3. A user-profile (remote application) that exposes a user information component.

We’ll use Vite for scaffolding our React projects due to its speed, but critically, we will integrate Webpack Module Federation using the @module-federation/webpack plugin, as Vite’s native module resolution doesn’t directly support the Webpack-specific federation protocol.

Setup: Project Structure

First, create a new directory for our project and navigate into it:

mkdir microfrontend-suite
cd microfrontend-suite

1. Create the Host Application (app-shell)

This will be our main application that pulls in other microfrontends.

# Create React app with Vite
npm create vite@latest app-shell -- --template react-ts
cd app-shell
npm install

Now, we need to configure Webpack Module Federation. We’ll install Webpack and the necessary plugins.

npm install --save-dev webpack webpack-cli webpack-dev-server html-webpack-plugin @module-federation/webpack @babel/core babel-loader @babel/preset-react @babel/preset-typescript css-loader style-loader

Explanation:

  • webpack, webpack-cli, webpack-dev-server: Standard Webpack tools.
  • html-webpack-plugin: Generates our index.html file.
  • @module-federation/webpack: The crucial plugin for Module Federation.
  • babel-loader, @babel/preset-react, @babel/preset-typescript: For transpiling React and TypeScript.
  • css-loader, style-loader: For handling CSS.

Next, create a webpack.config.js file in the app-shell directory.

// app-shell/webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { ModuleFederationPlugin } = require('@module-federation/webpack').default;
const path = require('path');

module.exports = {
  entry: './src/index.tsx', // Our main entry point
  mode: 'development',
  devServer: {
    port: 8080, // Host app will run on port 8080
    historyApiFallback: true, // For client-side routing
  },
  output: {
    publicPath: 'auto', // Important for Module Federation to resolve paths
  },
  resolve: {
    extensions: ['.tsx', '.ts', '.jsx', '.js', '.json'],
  },
  module: {
    rules: [
      {
        test: /\.(ts|tsx|js|jsx)$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-react', '@babel/preset-typescript'],
          },
        },
      },
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader'],
      },
    ],
  },
  plugins: [
    new ModuleFederationPlugin({
      name: 'app_shell', // Unique name for our host app
      remotes: {
        // Here we define the remotes we want to consume
        // The format is: 'remoteName': 'remoteName@http://localhost:port/remoteEntry.js'
        dashboard: 'dashboard_widget@http://localhost:8081/remoteEntry.js',
        profile: 'user_profile@http://localhost:8082/remoteEntry.js',
      },
      shared: {
        // Crucial for performance: share common dependencies
        // This ensures React and ReactDOM are loaded only once
        react: {
          singleton: true, // Only a single version of React should be loaded
          requiredVersion: '^18.0.0', // Specify compatible React version (adjust for 2026, e.g., '^19.0.0' or '^20.0.0')
        },
        'react-dom': {
          singleton: true,
          requiredVersion: '^18.0.0', // Same for ReactDOM
        },
        // Add other shared dependencies like react-router-dom, etc.
      },
    }),
    new HtmlWebpackPlugin({
      template: './public/index.html',
    }),
  ],
};

Explanation of app-shell/webpack.config.js:

  • name: 'app_shell': Identifies this application in the federation network.
  • remotes: This is where the host declares which remote applications it will consume. We specify dashboard and profile and their entry points.
  • shared: This is extremely important. By marking react and react-dom as singleton: true and specifying requiredVersion, we instruct Webpack to ensure that only one instance of these libraries is loaded into the browser, even if multiple microfrontends depend on them. This prevents duplicate bundles and potential runtime issues. For 2026, React ^18.0.0 is a safe baseline, but newer major versions like ^19.0.0 or ^20.0.0 would be common.

Update app-shell/public/index.html (if it exists, or create it):

<!-- app-shell/public/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Microfrontend App Shell</title>
</head>
<body>
    <div id="root"></div>
</body>
</html>

Update app-shell/src/index.tsx:

// app-shell/src/index.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';

const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

Update app-shell/src/App.tsx to include placeholder content for now:

// app-shell/src/App.tsx
import React from 'react';

const App: React.FC = () => {
  return (
    <div>
      <h1>Welcome to the App Shell!</h1>
      <p>Loading microfrontends soon...</p>
    </div>
  );
};

export default App;

Modify app-shell/package.json to add a start script:

// app-shell/package.json (add this script)
"scripts": {
  "start": "webpack serve --open",
  "build": "webpack --mode production"
},

2. Create Remote Application 1 (dashboard-widget)

This microfrontend will expose a simple widget.

# Go back to the root directory
cd ..
npm create vite@latest dashboard-widget -- --template react-ts
cd dashboard-widget
npm install

Install Webpack and Module Federation plugins, similar to the host:

npm install --save-dev webpack webpack-cli webpack-dev-server html-webpack-plugin @module-federation/webpack @babel/core babel-loader @babel/preset-react @babel/preset-typescript css-loader style-loader

Create dashboard-widget/webpack.config.js:

// dashboard-widget/webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { ModuleFederationPlugin } = require('@module-federation/webpack').default;
const path = require('path');

module.exports = {
  entry: './src/index.tsx',
  mode: 'development',
  devServer: {
    port: 8081, // This remote app will run on port 8081
    historyApiFallback: true,
  },
  output: {
    publicPath: 'auto',
  },
  resolve: {
    extensions: ['.tsx', '.ts', '.jsx', '.js', '.json'],
  },
  module: {
    rules: [
      {
        test: /\.(ts|tsx|js|jsx)$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-react', '@babel/preset-typescript'],
          },
        },
      },
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader'],
      },
    ],
  },
  plugins: [
    new ModuleFederationPlugin({
      name: 'dashboard_widget', // Unique name for this remote
      filename: 'remoteEntry.js', // The file the host will load
      exposes: {
        // What we expose from this remote
        './DashboardWidget': './src/DashboardWidget.tsx',
      },
      shared: {
        react: {
          singleton: true,
          requiredVersion: '^18.0.0', // Match host's required version
        },
        'react-dom': {
          singleton: true,
          requiredVersion: '^18.0.0',
        },
      },
    }),
    new HtmlWebpackPlugin({
      template: './public/index.html',
    }),
  ],
};

Explanation of dashboard-widget/webpack.config.js:

  • name: 'dashboard_widget': Unique identifier for this remote.
  • filename: 'remoteEntry.js': The name of the JavaScript bundle that contains the exposed modules and the federation runtime logic.
  • exposes: This is where we declare which modules (components in our case) from this application are made available to others. ./DashboardWidget maps to the component located at ./src/DashboardWidget.tsx.
  • shared: Again, we share react and react-dom to ensure singletons.

Create dashboard-widget/public/index.html:

<!-- dashboard-widget/public/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Dashboard Widget Remote</title>
</head>
<body>
    <div id="root"></div>
</body>
</html>

Update dashboard-widget/src/index.tsx:

// dashboard-widget/src/index.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import DashboardWidget from './DashboardWidget';

// This remote app can also be run independently for development
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
root.render(
  <React.StrictMode>
    <h2>Running Dashboard Widget Independently</h2>
    <DashboardWidget />
  </React.StrictMode>
);

Create dashboard-widget/src/DashboardWidget.tsx (the component we’ll expose):

// dashboard-widget/src/DashboardWidget.tsx
import React from 'react';

const DashboardWidget: React.FC = () => {
  const data = {
    users: 12345,
    revenue: '$56,789',
    activeSessions: 789,
  };

  return (
    <div style={{ padding: '20px', border: '1px solid #ccc', borderRadius: '8px', margin: '10px', backgroundColor: '#f9f9f9' }}>
      <h3>๐Ÿ“Š Sales Dashboard Widget</h3>
      <p>Total Users: <strong>{data.users}</strong></p>
      <p>Monthly Revenue: <strong>{data.revenue}</strong></p>
      <p>Active Sessions: <strong>{data.activeSessions}</strong></p>
      <button onClick={() => alert('Refreshing data from Dashboard Widget!')}>Refresh Data</button>
    </div>
  );
};

export default DashboardWidget;

Modify dashboard-widget/package.json to add a start script:

// dashboard-widget/package.json (add this script)
"scripts": {
  "start": "webpack serve --open",
  "build": "webpack --mode production"
},

3. Create Remote Application 2 (user-profile)

This will expose a user profile component.

# Go back to the root directory
cd ..
npm create vite@latest user-profile -- --template react-ts
cd user-profile
npm install

Install Webpack and Module Federation plugins:

npm install --save-dev webpack webpack-cli webpack-dev-server html-webpack-plugin @module-federation/webpack @babel/core babel-loader @babel/preset-react @babel/preset-typescript css-loader style-loader

Create user-profile/webpack.config.js:

// user-profile/webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { ModuleFederationPlugin } = require('@module-federation/webpack').default;
const path = require('path');

module.exports = {
  entry: './src/index.tsx',
  mode: 'development',
  devServer: {
    port: 8082, // This remote app will run on port 8082
    historyApiFallback: true,
  },
  output: {
    publicPath: 'auto',
  },
  resolve: {
    extensions: ['.tsx', '.ts', '.jsx', '.js', '.json'],
  },
  module: {
    rules: [
      {
        test: /\.(ts|tsx|js|jsx)$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-react', '@babel/preset-typescript'],
          },
        },
      },
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader'],
      },
    ],
  },
  plugins: [
    new ModuleFederationPlugin({
      name: 'user_profile',
      filename: 'remoteEntry.js',
      exposes: {
        './UserProfile': './src/UserProfile.tsx',
      },
      shared: {
        react: {
          singleton: true,
          requiredVersion: '^18.0.0',
        },
        'react-dom': {
          singleton: true,
          requiredVersion: '^18.0.0',
        },
      },
    }),
    new HtmlWebpackPlugin({
      template: './public/index.html',
    }),
  ],
};

Create user-profile/public/index.html:

<!-- user-profile/public/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>User Profile Remote</title>
</head>
<body>
    <div id="root"></div>
</body>
</html>

Update user-profile/src/index.tsx:

// user-profile/src/index.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import UserProfile from './UserProfile';

const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
root.render(
  <React.StrictMode>
    <h2>Running User Profile Independently</h2>
    <UserProfile />
  </React.StrictMode>
);

Create user-profile/src/UserProfile.tsx:

// user-profile/src/UserProfile.tsx
import React from 'react';

const UserProfile: React.FC = () => {
  const user = {
    name: 'Alice Wonderland',
    email: '[email protected]',
    role: 'Administrator',
    lastLogin: '2026-02-14 10:30 AM',
  };

  return (
    <div style={{ padding: '20px', border: '1px solid #ccc', borderRadius: '8px', margin: '10px', backgroundColor: '#eef' }}>
      <h3>๐Ÿ‘ค User Profile</h3>
      <p>Name: <strong>{user.name}</strong></p>
      <p>Email: <strong>{user.email}</strong></p>
      <p>Role: <strong>{user.role}</strong></p>
      <p>Last Login: <strong>{user.lastLogin}</strong></p>
    </div>
  );
};

export default UserProfile;

Modify user-profile/package.json to add a start script:

// user-profile/package.json (add this script)
"scripts": {
  "start": "webpack serve --open",
  "build": "webpack --mode production"
},

4. Integrate Remotes into the Host (app-shell)

Now that our remotes are set up, let’s update app-shell/src/App.tsx to dynamically load and render them.

First, we need to declare the types for our dynamically loaded remotes so TypeScript knows what to expect. Create a file app-shell/src/declarations.d.ts:

// app-shell/src/declarations.d.ts
declare module 'dashboard/DashboardWidget' {
  const DashboardWidget: React.ComponentType;
  export default DashboardWidget;
}

declare module 'profile/UserProfile' {
  const UserProfile: React.ComponentType;
  export default UserProfile;
}

Now, modify app-shell/src/App.tsx to use these remote components:

// app-shell/src/App.tsx
import React, { Suspense } from 'react';

// Dynamically import the remote components
// The syntax 'remoteName/exposedModuleName' corresponds to
// the 'remotes' config in webpack.config.js and 'exposes' config in remote's webpack.config.js
const DashboardWidget = React.lazy(() => import('dashboard/DashboardWidget'));
const UserProfile = React.lazy(() => import('profile/UserProfile'));

const App: React.FC = () => {
  return (
    <div style={{ fontFamily: 'Arial, sans-serif', padding: '20px' }}>
      <h1 style={{ color: '#333' }}>Welcome to the Microfrontend App Shell!</h1>
      <p>This is where we integrate all our independent components.</p>

      <div style={{ display: 'flex', gap: '20px', marginTop: '30px' }}>
        {/* Use Suspense to handle loading states for dynamically imported components */}
        <Suspense fallback={<div>Loading Dashboard Widget...</div>}>
          <DashboardWidget />
        </Suspense>

        <Suspense fallback={<div>Loading User Profile...</div>}>
          <UserProfile />
        </Suspense>
      </div>

      <footer style={{ marginTop: '50px', paddingTop: '20px', borderTop: '1px solid #eee', fontSize: '0.9em', color: '#666' }}>
        Powered by Webpack Module Federation
      </footer>
    </div>
  );
};

export default App;

Explanation of app-shell/src/App.tsx:

  • React.lazy(): This function lets you render a dynamic import as a regular component. It’s built for code-splitting and works perfectly with Module Federation.
  • Suspense: A React component that lets you “wait” for some code to load and specify a loading indicator (fallback) while it’s happening. If the remote module fails to load, you might want to wrap this in an Error Boundary for a more robust solution (as discussed in Chapter 6: Error Handling).

Running the Microfrontend Suite

To see this in action, you need to start all three applications simultaneously. Open three separate terminal windows:

  1. Terminal 1 (Dashboard Widget):

    cd microfrontend-suite/dashboard-widget
    npm start
    

    This will start the dashboard-widget on http://localhost:8081.

  2. Terminal 2 (User Profile):

    cd microfrontend-suite/user-profile
    npm start
    

    This will start the user-profile on http://localhost:8082.

  3. Terminal 3 (App Shell):

    cd microfrontend-suite/app-shell
    npm start
    

    This will start the app-shell on http://localhost:8080.

Now, open your browser and navigate to http://localhost:8080. You should see the app-shell loading and displaying both the “Sales Dashboard Widget” and the “User Profile” components, seamlessly integrated from their independent applications! If you stop one of the remote applications, you’ll see the Suspense fallback message appear, demonstrating the dynamic loading.


Mini-Challenge: Add a New Settings Microfrontend

Your turn! Let’s reinforce your understanding by adding another microfrontend.

Challenge: Create a new microfrontend called app-settings that exposes a simple “Settings” component. Then, integrate this Settings component into your app-shell alongside the existing dashboard and profile widgets.

Steps:

  1. Create a new React project named app-settings.
  2. Install Webpack and Module Federation dependencies.
  3. Configure app-settings/webpack.config.js to expose a Settings component (e.g., ./SettingsPage). Remember to set its port (e.g., 8083) and configure name, filename, exposes, and shared.
  4. Create a simple app-settings/src/SettingsPage.tsx component.
  5. Update app-shell/webpack.config.js to include app-settings in its remotes configuration.
  6. Add a type declaration for the new settings module in app-shell/src/declarations.d.ts.
  7. Modify app-shell/src/App.tsx to dynamically import and render the SettingsPage component, using React.lazy and Suspense.
  8. Start all four applications and verify the integration.

Hint: Follow the exact pattern used for dashboard-widget and user-profile. Pay close attention to unique port numbers, name properties, and the remotes/exposes mapping.

What to Observe/Learn:

  • You’ll solidify your understanding of the host-remote relationship and the configuration required for each.
  • You’ll appreciate how easily new features (as microfrontends) can be added to the existing shell without touching the other remote applications.

Common Pitfalls & Troubleshooting

Working with microfrontends and Module Federation can introduce new complexities. Here are some common issues and how to tackle them:

  1. Shared Dependency Version Mismatches:

    • Problem: If the host and a remote both try to load different major versions of a shared library (e.g., React 17 and React 18), you can get runtime errors, duplicate bundles, or unexpected behavior.
    • Solution: Use the shared configuration diligently.
      • singleton: true: Ensures only one instance of the dependency is loaded.
      • requiredVersion: '^18.0.0': Specifies a compatible semantic version range. If a remote requires a version outside this range, Webpack will either warn or error, depending on the strictVersion flag.
      • strictVersion: true: (Default false) If set to true, Webpack will throw an error if the requiredVersion doesn’t strictly match the available shared version. This is safer for critical dependencies.
    • Debugging: Check your browser’s network tab. If you see multiple react.js or react-dom.js bundles, you likely have a shared dependency issue. Also, inspect the console for Webpack Module Federation warnings about version conflicts.
  2. Loading Errors / Network Issues:

    • Problem: A remote application might not be running, its remoteEntry.js file might be inaccessible, or there could be a network issue. This leads to broken UI in the host.
    • Solution:
      • Suspense fallback: As demonstrated, Suspense provides a basic loading state.
      • Error Boundaries: Wrap your dynamically loaded remote components in React Error Boundaries. This allows you to gracefully catch loading failures or runtime errors originating from the remote and display a user-friendly message or fallback UI, preventing the entire host application from crashing.
      • Network Tab: Always check the network tab in your browser’s developer tools to see if remoteEntry.js files are being loaded correctly and without HTTP errors.
      • CORS: Ensure your webpack-dev-server for each remote is configured to allow CORS requests from the host’s origin if they are on different domains/ports. In our example, publicPath: 'auto' and default dev server settings usually handle this for local development.
  3. Routing Conflicts and History Management:

    • Problem: If both the host and remotes use client-side routing (e.g., react-router-dom), they can interfere with each other’s history stack or render the wrong component.
    • Solution:
      • Shared Router: Often, the host application owns the primary router. Remotes can then register their routes with the host’s router or use relative paths within their isolated context.
      • Abstracted Routing: Create a shared routing utility or context that the host provides, allowing remotes to navigate or define sub-routes without directly managing the browser history.
      • Event-Driven Navigation: Remotes can emit events (e.g., using a custom event bus or shared state management) that the host listens to for navigation instructions.
  4. Performance Overheads:

    • Problem: If not managed carefully, microfrontends can lead to larger total bundle sizes due to duplicated dependencies or inefficient code splitting.
    • Solution:
      • Aggressive shared configuration: Share as many common libraries as possible.
      • Lazy Loading: Use React.lazy() and Suspense for all remote components to ensure they are only loaded when needed.
      • Code Splitting within Remotes: Remotes should also implement their own code splitting to reduce their initial bundle size.
      • Caching: Leverage browser caching and CDN delivery for remoteEntry.js files.

Summary

Congratulations! You’ve successfully navigated the exciting world of microfrontends with Webpack Module Federation. Let’s recap the key takeaways:

  • Microfrontends break down large frontend monoliths into smaller, independently deployable applications, fostering team autonomy and accelerating development.
  • Webpack Module Federation provides a native, powerful mechanism to achieve runtime composition of these independent applications.
  • The Host application consumes modules exposed by Remote applications.
  • The ModuleFederationPlugin with name, filename, exposes, remotes, and crucially, shared configurations are central to this pattern.
  • Shared dependencies (like React and ReactDOM) are vital for performance and stability, preventing duplicate downloads and runtime conflicts.
  • React.lazy and Suspense are essential for dynamically loading remote components and providing a smooth user experience.
  • While offering immense benefits, microfrontends introduce complexities in areas like shared state, routing, and operational management, requiring careful architectural consideration.

You’ve now got a foundational understanding of how to build truly scalable and decoupled frontend systems. This architectural pattern is increasingly becoming a standard for large-scale enterprise applications in 2026, enabling teams to move faster and with greater confidence.

In the next chapter, we’ll delve into managing large-scale routing and state boundaries within such complex applications, ensuring that even with many independent parts, your users experience a cohesive and predictable flow.


References


This page is AI-assisted and reviewed. It references official documentation and recognized resources where relevant.