Skip to main content
Module Federation allows multiple independent webpack builds to share code at runtime, enabling micro-frontend architectures and dynamic code sharing without rebuilding.

What is Module Federation?

Module Federation enables:
  • Multiple separate builds sharing modules at runtime
  • Independent deployments of micro-frontends
  • Sharing dependencies without duplication
  • Dynamic remote module loading
  • No build-time dependencies between apps
Module Federation is a webpack 5 feature. Both the host and remote applications must use webpack 5.

Core Concepts

Host

The application that consumes remote modules.

Remote

The application that exposes modules for others to consume.

Bidirectional

An application can be both host and remote simultaneously.

Basic Setup

1

Create a remote application

remote/webpack.config.js:
const { ModuleFederationPlugin } = require('webpack').container;

module.exports = {
  name: 'remote',
  mode: 'development',
  
  output: {
    publicPath: 'http://localhost:3001/',
    uniqueName: 'remote'
  },
  
  plugins: [
    new ModuleFederationPlugin({
      name: 'remote',
      filename: 'remoteEntry.js',
      
      exposes: {
        './Button': './src/Button',
        './Header': './src/Header'
      },
      
      shared: {
        react: { singleton: true },
        'react-dom': { singleton: true }
      }
    })
  ],
  
  devServer: {
    port: 3001
  }
};
2

Create a host application

host/webpack.config.js:
const { ModuleFederationPlugin } = require('webpack').container;

module.exports = {
  name: 'host',
  mode: 'development',
  
  output: {
    publicPath: 'http://localhost:3000/',
    uniqueName: 'host'
  },
  
  plugins: [
    new ModuleFederationPlugin({
      name: 'host',
      
      remotes: {
        remote: 'remote@http://localhost:3001/remoteEntry.js'
      },
      
      shared: {
        react: { singleton: true },
        'react-dom': { singleton: true }
      }
    })
  ],
  
  devServer: {
    port: 3000
  }
};
3

Use remote modules in host

import React, { lazy, Suspense } from 'react';

const RemoteButton = lazy(() => import('remote/Button'));
const RemoteHeader = lazy(() => import('remote/Header'));

function App() {
  return (
    <div>
      <Suspense fallback="Loading Header...">
        <RemoteHeader />
      </Suspense>
      
      <Suspense fallback="Loading Button...">
        <RemoteButton />
      </Suspense>
    </div>
  );
}

Configuration Options

Exposing Modules

new ModuleFederationPlugin({
  name: 'myApp',
  filename: 'remoteEntry.js',
  
  exposes: {
    './Component': './src/Component',
    './utils': './src/utils',
    './hooks/useAuth': './src/hooks/useAuth'
  }
})

Consuming Remotes

new ModuleFederationPlugin({
  remotes: {
    app1: 'app1@http://localhost:3001/remoteEntry.js',
    app2: 'app2@http://localhost:3002/remoteEntry.js',
    
    // Dynamic remote URLs
    app3: `app3@${process.env.APP3_URL}/remoteEntry.js`
  }
})

Shared Dependencies

new ModuleFederationPlugin({
  shared: {
    // Simple sharing
    react: { singleton: true },
    
    // With version requirements
    lodash: {
      singleton: true,
      requiredVersion: '^4.17.0'
    },
    
    // Share all lodash modules
    'lodash/': {},
    
    // Advanced configuration
    'react-router-dom': {
      singleton: true,
      requiredVersion: '^6.0.0',
      strictVersion: true,
      eager: false
    }
  }
})
Use singleton: true for libraries that must have only one instance (like React, Vue, or global state managers).

Shared Module Options

OptionDescription
singletonOnly one version of the shared module
requiredVersionMinimum version requirement
strictVersionThrow error if version doesn’t match
eagerInclude in initial chunk (not async)
importModule to provide (default is key)
shareKeyKey to use for sharing
shareScopeSharing scope name

Real-World Example

Micro-frontend architecture with shared dependencies: Shell Application (Host):
const { ModuleFederationPlugin } = require('webpack').container;

module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'shell',
      
      remotes: {
        products: 'products@https://products.example.com/remoteEntry.js',
        checkout: 'checkout@https://checkout.example.com/remoteEntry.js',
        profile: 'profile@https://profile.example.com/remoteEntry.js'
      },
      
      shared: {
        react: {
          singleton: true,
          requiredVersion: '^18.0.0',
          strictVersion: true
        },
        'react-dom': {
          singleton: true,
          requiredVersion: '^18.0.0',
          strictVersion: true
        },
        'react-router-dom': {
          singleton: true,
          requiredVersion: '^6.0.0'
        }
      }
    })
  ]
};
Products Micro-Frontend (Remote):
const { ModuleFederationPlugin } = require('webpack').container;

module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'products',
      filename: 'remoteEntry.js',
      
      exposes: {
        './ProductList': './src/components/ProductList',
        './ProductDetail': './src/components/ProductDetail',
        './routes': './src/routes'
      },
      
      shared: {
        react: { singleton: true },
        'react-dom': { singleton: true },
        'react-router-dom': { singleton: true }
      }
    })
  ]
};
Using in Shell:
import React, { lazy, Suspense } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';

const ProductList = lazy(() => import('products/ProductList'));
const ProductDetail = lazy(() => import('products/ProductDetail'));
const Checkout = lazy(() => import('checkout/CheckoutPage'));
const Profile = lazy(() => import('profile/ProfilePage'));

function App() {
  return (
    <BrowserRouter>
      <Suspense fallback={<div>Loading...</div>}>
        <Routes>
          <Route path="/products" element={<ProductList />} />
          <Route path="/products/:id" element={<ProductDetail />} />
          <Route path="/checkout" element={<Checkout />} />
          <Route path="/profile" element={<Profile />} />
        </Routes>
      </Suspense>
    </BrowserRouter>
  );
}

Dynamic Remotes

Load remotes dynamically at runtime:
// webpack.config.js
new ModuleFederationPlugin({
  name: 'host',
  remotes: {
    // Use script tag approach for dynamic URLs
  }
})

// Dynamic remote loader
function loadRemote(url, scope, module) {
  return async () => {
    await __webpack_init_sharing__('default');
    
    const container = await new Promise((resolve, reject) => {
      const script = document.createElement('script');
      script.src = url;
      script.onload = () => {
        const proxy = {
          get: (request) => window[scope].get(request),
          init: (arg) => {
            try {
              return window[scope].init(arg);
            } catch (e) {
              console.error('Remote container init failed:', e);
            }
          }
        };
        resolve(proxy);
      };
      script.onerror = reject;
      document.head.appendChild(script);
    });
    
    await container.init(__webpack_share_scopes__.default);
    const factory = await container.get(module);
    return factory();
  };
}

// Usage
const RemoteComponent = React.lazy(
  loadRemote(
    'https://example.com/remoteEntry.js',
    'remoteName',
    './Component'
  )
);

Bidirectional Sharing

Both applications can expose and consume modules: App A:
new ModuleFederationPlugin({
  name: 'appA',
  filename: 'remoteEntry.js',
  
  exposes: {
    './Header': './src/Header'
  },
  
  remotes: {
    appB: 'appB@http://localhost:3002/remoteEntry.js'
  },
  
  shared: ['react', 'react-dom']
})
App B:
new ModuleFederationPlugin({
  name: 'appB',
  filename: 'remoteEntry.js',
  
  exposes: {
    './Footer': './src/Footer'
  },
  
  remotes: {
    appA: 'appA@http://localhost:3001/remoteEntry.js'
  },
  
  shared: ['react', 'react-dom']
})

Version Management

Automatic Version Detection

const deps = require('./package.json').dependencies;

new ModuleFederationPlugin({
  shared: {
    ...deps,
    react: {
      singleton: true,
      requiredVersion: deps.react
    }
  }
})

Fallback Loading

new ModuleFederationPlugin({
  shared: {
    react: {
      singleton: true,
      requiredVersion: '^18.0.0',
      // Don't fail if not available
      strictVersion: false
    }
  }
})

Error Handling

import React, { lazy, Suspense } from 'react';

class ErrorBoundary extends React.Component {
  state = { hasError: false };
  
  static getDerivedStateFromError(error) {
    return { hasError: true };
  }
  
  componentDidCatch(error, errorInfo) {
    console.error('Remote module failed:', error, errorInfo);
  }
  
  render() {
    if (this.state.hasError) {
      return <div>Failed to load remote module</div>;
    }
    return this.props.children;
  }
}

const RemoteComponent = lazy(() => 
  import('remote/Component').catch(() => {
    // Fallback component
    return { default: () => <div>Fallback UI</div> };
  })
);

function App() {
  return (
    <ErrorBoundary>
      <Suspense fallback="Loading...">
        <RemoteComponent />
      </Suspense>
    </ErrorBoundary>
  );
}

TypeScript Support

Type Declarations for Remotes

src/@types/remote.d.ts:
declare module 'remote/Button' {
  const Button: React.FC<{
    onClick: () => void;
    children: React.ReactNode;
  }>;
  export default Button;
}

declare module 'remote/Header' {
  const Header: React.FC<{
    title: string;
  }>;
  export default Header;
}

Generate Types Automatically

npm install --save-dev @module-federation/typescript

Best Practices

1

Use singleton for UI libraries

shared: {
  react: { singleton: true },
  'react-dom': { singleton: true }
}
2

Set unique names

output: {
  uniqueName: 'my-unique-app-name'
}
3

Handle loading errors

Always wrap remote components in ErrorBoundary
4

Version your remoteEntry

filename: 'remoteEntry.[contenthash].js'
5

Use Suspense for loading states

<Suspense fallback={<Spinner />}>
  <RemoteComponent />
</Suspense>

Common Patterns

Shared Component Library

// Design system remote
new ModuleFederationPlugin({
  name: 'designSystem',
  filename: 'remoteEntry.js',
  exposes: {
    './Button': './src/Button',
    './Input': './src/Input',
    './Modal': './src/Modal',
    './theme': './src/theme'
  }
})

Shared State Management

// State remote
new ModuleFederationPlugin({
  name: 'store',
  exposes: {
    './store': './src/store',
    './actions': './src/actions',
    './selectors': './src/selectors'
  },
  shared: {
    redux: { singleton: true },
    'react-redux': { singleton: true }
  }
})
Be careful with shared state across micro-frontends. Consider using events or a shared state library instead.

Performance Considerations

  • Minimize shared dependencies to reduce bundle duplication
  • Use code splitting within each remote
  • Cache remoteEntry.js appropriately
  • Monitor bundle sizes with webpack-bundle-analyzer
Module Federation works best when remotes are deployed independently but share common dependencies like React, Vue, or utility libraries.