SalakCode SalakCode
Bundling

Module Federation

Share code between independent builds

Advanced
bundling webpack micro-frontends runtime

Definition

Module Federation is a webpack feature that allows JavaScript applications to dynamically import code from other independently deployed applications at runtime. It enables micro-frontend architectures, plugin systems, and runtime sharing of components without building everything together.

How It Works

Traditional Build:              Module Federation:
┌──────────────┐                ┌─────────────┐      ┌─────────────┐
│   Build      │                │   Host      │◄────►│   Remote    │
│  Everything  │                │   (Shell)   │      │   (MFE)     │
│   Together   │                └──────┬──────┘      └─────────────┘
└──────────────┘                       │
                                       │    ┌─────────────┐
                                       └───►│   Remote 2  │
                                            └─────────────┘

One big bundle                Multiple independent bundles
                              Shared at runtime

Basic Configuration

Host (Consumer)

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

module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'host',
      remotes: {
        // Load from remote at runtime
        app1: 'app1@https://app1.example.com/remoteEntry.js',
        app2: 'app2@https://app2.example.com/remoteEntry.js',
      },
      shared: {
        // Share dependencies
        react: { singleton: true },
        'react-dom': { singleton: true },
      },
    }),
  ],
};

// host/src/App.js
import React, { Suspense } from 'react';

// Dynamic import from remote
const RemoteApp1 = React.lazy(() => import('app1/App'));
const RemoteApp2 = React.lazy(() => import('app2/App'));

function App() {
  return (
    <div>
      <Header />
      <Suspense fallback={<div>Loading...</div>}>
        <RemoteApp1 />
        <RemoteApp2 />
      </Suspense>
    </div>
  );
}

Remote (Provider)

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

module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'app1',
      filename: 'remoteEntry.js',
      exposes: {
        // Expose modules to host
        './App': './src/App',
        './Button': './src/components/Button',
        './utils': './src/utils',
      },
      shared: {
        // Match host's shared deps
        react: { singleton: true },
        'react-dom': { singleton: true },
      },
    }),
  ],
};

Shared Dependencies

Singleton Pattern

shared: {
  react: { 
    singleton: true,
    requiredVersion: '^18.0.0',
    // If version mismatch:
    // - eager: true (load immediately)
    // - fallback to own version
  },
  'react-dom': { singleton: true },
}

// Only one React instance loaded
// Prevents "multiple React versions" errors

Version Management

shared: {
  lodash: {
    singleton: false,  // Multiple versions OK
    requiredVersion: '^4.17.0',
  },
  '@company/ui': {
    singleton: true,
    requiredVersion: '^1.0.0',
    // strictVersion: true (enforce exact match)
  }
}

Advanced Patterns

Dynamic Remotes

// Load remote at runtime
async function loadRemote(remoteName, remoteUrl) {
  await __webpack_init_sharing__('default');
  
  const container = window[remoteName];
  await container.init(__webpack_share_scopes__.default);
  
  const factory = await container.get('./App');
  return factory();
}

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

Bi-Directional Sharing

// Both apps can be host and remote
// App A exposes components, consumes from App B
// App B exposes components, consumes from App A

// Circular dependencies possible but beware complexity

Plugin Architecture

// Host loads plugins dynamically
const plugins = await fetch('/api/plugins');

for (const plugin of plugins) {
  const PluginComponent = React.lazy(() => 
    import(/* webpackIgnore: true */ plugin.url)
  );
  render(<PluginComponent />);
}

Framework-Specific

Next.js

// next.config.js
const NextFederationPlugin = require('@module-federation/nextjs-mf');

module.exports = {
  webpack(config, options) {
    config.plugins.push(
      new NextFederationPlugin({
        name: 'host',
        filename: 'static/chunks/remoteEntry.js',
        remotes: {
          shop: 'shop@https://shop.example.com/_next/static/chunks/remoteEntry.js',
        },
        shared: {},
      })
    );
    return config;
  },
};

TypeScript

// types/remotes.d.ts
declare module 'app1/App' {
  const App: React.ComponentType;
  export default App;
}

declare module 'app1/Button' {
  interface ButtonProps {
    children: React.ReactNode;
    onClick?: () => void;
  }
  const Button: React.FC<ButtonProps>;
  export default Button;
}

Deployment Strategy

Independent Deployments

# Each app deploys separately
app1:
  deploy: production
  url: https://app1.example.com/remoteEntry.js
  
app2:
  deploy: production  
  url: https://app2.example.com/remoteEntry.js

host:
  deploy: production
  remotes:
    app1: https://app1.example.com/remoteEntry.js
    app2: https://app2.example.com/remoteEntry.js

Version Management

// Remote exposes version
new ModuleFederationPlugin({
  name: 'app1',
  exposes: {
    './version': './package.json',
  }
});

// Host checks compatibility
const { version } = await import('app1/version');
if (!satisfies(version, '^1.0.0')) {
  console.warn('Incompatible version');
}

Common Issues

Chunk Loading

// Ensure publicPath is set
output: {
  publicPath: 'auto', // Or specific URL
}

// For dynamic remotes
__webpack_public_path__ = document.currentScript.src + '/../';

React Context

// Context must be shared
shared: {
  react: { singleton: true, eager: true },
}

// Or use external context provider
// Wrap remote with context from host

Routing

// Use memory router in remotes
// Sync with host router via events
import { MemoryRouter } from 'react-router-dom';

function RemoteApp() {
  return (
    <MemoryRouter initialEntries={[window.location.pathname]}>
      <Routes>...</Routes>
    </MemoryRouter>
  );
}

Best Practices

1. Define Clear Contracts

// Expose minimal, stable API
exposes: {
  './App': './src/App',        // Component
  './routes': './src/routes',  // Route config
  './store': './src/store',    // State management
}

2. Error Boundaries

// Wrap remotes in error boundaries
class RemoteErrorBoundary extends React.Component {
  state = { hasError: false };
  
  static getDerivedStateFromError() {
    return { hasError: true };
  }
  
  render() {
    if (this.state.hasError) {
      return <div>Failed to load remote</div>;
    }
    return this.props.children;
  }
}

3. Loading States

<Suspense fallback={<Skeleton />}>
  <ErrorBoundary>
    <RemoteApp />
  </ErrorBoundary>
</Suspense>
Key Takeaway

Module Federation enables runtime code sharing between independent builds. Use it for micro-frontends, plugin systems, or gradual migrations. Manage shared dependencies carefully to avoid version conflicts, and implement error boundaries for resilience. It’s powerful but adds complexity—use only when independent deployment is required.

Resources

Related Topics