Module Federation has transformed how we build large-scale frontend applications by enabling true micro-frontend architectures. Let's explore how to implement this powerful feature and create scalable, distributed frontend systems.
Understanding Module Federation 🏗️
Module Federation allows multiple independent applications to share code and dependencies in real-time. This architectural pattern enables:
- Independent deployment of frontend applications
- Runtime dependency sharing
- Reduced bundle sizes
- Team autonomy and scalability
Basic Setup
First, let's configure a host application:
// webpack.config.js (host)
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'host',
filename: 'remoteEntry.js',
remotes: {
app1: 'app1@http://localhost:3001/remoteEntry.js',
app2: 'app2@http://localhost:3002/remoteEntry.js'
},
shared: ['react', 'react-dom']
})
]
};
Now, configure a remote application:
// webpack.config.js (remote)
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'app1',
filename: 'remoteEntry.js',
exposes: {
'./Button': './src/components/Button',
'./Header': './src/components/Header'
},
shared: ['react', 'react-dom']
})
]
};
Implementing Shared Components
Create reusable components in your remote application:
// Button.jsx
import React from 'react';
const Button = ({ text, onClick }) => {
return (
<button
className="shared-button"
onClick={onClick}
>
{text}
</button>
);
};
export default Button;
Loading Remote Components
Import and use remote components in your host application:
// App.jsx (host)
import React, { Suspense } from 'react';
const RemoteButton = React.lazy(() => import('app1/Button'));
const App = () => {
return (
<div>
<h1>Host Application</h1>
<Suspense fallback="Loading Button...">
<RemoteButton
text="Click Me"
onClick={() => console.log('Clicked!')}
/>
</Suspense>
</div>
);
};
Advanced Patterns
Error Boundaries
Implement error handling for remote module loading:
// ErrorBoundary.jsx
class ErrorBoundary extends React.Component {
state = { hasError: false };
static getDerivedStateFromError(error) {
return { hasError: true };
}
render() {
if (this.state.hasError) {
return this.props.fallback;
}
return this.props.children;
}
}
Dynamic Remote Loading
Implement dynamic remote loading for better performance:
const loadComponent = async (scope, module) => {
await __webpack_init_sharing__('default');
const container = window[scope];
await container.init(__webpack_share_scopes__.default);
const factory = await container.get(module);
return factory();
};
State Management
Shared State Pattern
Implement shared state between micro-frontends:
// store.js
import create from 'zustand';
const useStore = create((set) => ({
user: null,
setUser: (user) => set({ user }),
logout: () => set({ user: null })
}));
export default useStore;
Deployment Strategies
Progressive Loading
const loadRemote = (remoteUrl, scope, module) => { return new Promise((resolve, reject) => { const script = document.createElement('script'); script.src = remoteUrl; script.onload = () => { const proxy = { get: (request) => window[scope].get(request), init: (arg) => { try { return window[scope].init(arg); } catch(e) { console.log('Remote container already initialized'); } } }; resolve(proxy); } document.head.appendChild(script); }); };
Version Control
// webpack.config.js new ModuleFederationPlugin({ name: 'app1', filename: 'remoteEntry.js', exposes: { './Button': './src/components/Button' }, shared: { react: { singleton: true, requiredVersion: '^17.0.0' } } });
Performance Optimization 🚀
- Shared Dependencies Management
- Use singleton mode for framework dependencies
- Implement version control for shared modules
- Optimize chunk splitting
Caching Strategy
// webpack.config.js module.exports = { output: { filename: '[name].[contenthash].js', publicPath: 'auto' }, optimization: { runtimeChunk: false, splitChunks: { chunks: 'all' } } };
Monitoring and Analytics
Implement performance monitoring:
const trackModuleLoad = (moduleName) => {
const startTime = performance.now();
return {
success: () => {
const duration = performance.now() - startTime;
console.log(`Module ${moduleName} loaded in ${duration}ms`);
},
error: (err) => {
console.error(`Failed to load ${moduleName}:`, err);
}
};
};
Best Practices
- Module Design
- Keep shared modules small and focused
- Implement clear contracts between modules
- Use TypeScript for better type safety
- Testing Strategy
- Test modules in isolation
- Implement integration tests
- Use mock federation plugins for testing
- Security Considerations
- Implement CSP headers
- Validate remote sources
- Monitor third-party dependencies
Conclusion
Module Federation enables powerful micro-frontend architectures while maintaining flexibility and scalability. By following these patterns and best practices, you can build robust, maintainable distributed frontend systems that scale with your organization's needs.
Remember to:
- Start with a clear architecture plan
- Implement proper error handling
- Monitor performance metrics
- Maintain good documentation
- Regular security audits
As the ecosystem continues to evolve, staying updated with the latest patterns and best practices will help you make the most of Module Federation in your applications.