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.