Why Micro Frontends?
As frontend applications grow, they become difficult to maintain. Micro frontends allow teams to work independently, deploy separately, and use different technologies.
Module Federation (Webpack 5)
Host Application
// webpack.config.js (Host)
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'host',
remotes: {
// Load remote micro frontends
products: 'products@http://localhost:3001/remoteEntry.js',
checkout: 'checkout@http://localhost:3002/remoteEntry.js',
profile: 'profile@http://localhost:3003/remoteEntry.js'
},
shared: {
react: { singleton: true, requiredVersion: '^18.0.0' },
'react-dom': { singleton: true, requiredVersion: '^18.0.0' }
}
})
]
};Remote Application
// webpack.config.js (Products micro frontend)
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'products',
filename: 'remoteEntry.js',
exposes: {
'./ProductList': './src/components/ProductList',
'./ProductDetail': './src/components/ProductDetail'
},
shared: {
react: { singleton: true },
'react-dom': { singleton: true }
}
})
]
};Loading Remote Components
// In host application
import React, { Suspense, lazy } from 'react';
// Dynamic import of remote component
const ProductList = lazy(() => import('products/ProductList'));
const Checkout = lazy(() => import('checkout/CheckoutForm'));
function App() {
return (
Loading products... }>
Loading checkout...}>
);
} Single-SPA Approach
// Root config
import { registerApplication, start } from 'single-spa';
registerApplication({
name: '@myorg/navbar',
app: () => System.import('@myorg/navbar'),
activeWhen: () => true // Always active
});
registerApplication({
name: '@myorg/products',
app: () => System.import('@myorg/products'),
activeWhen: (location) => location.pathname.startsWith('/products')
});
registerApplication({
name: '@myorg/checkout',
app: () => System.import('@myorg/checkout'),
activeWhen: '/checkout'
});
start();// Products micro frontend
import { registerApplication } from 'single-spa';
import singleSpaReact from 'single-spa-react';
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
const lifecycles = singleSpaReact({
React,
ReactDOM,
rootComponent: App,
domElementGetter: () => document.getElementById('products-container')
});
export const { bootstrap, mount, unmount } = lifecycles;Communication Between Micro Frontends
// Custom event bus
class EventBus {
private events: Record = {};
subscribe(event: string, callback: Function) {
this.events[event] = this.events[event] || [];
this.events[event].push(callback);
return () => {
this.events[event] = this.events[event].filter(cb => cb !== callback);
};
}
publish(event: string, data: any) {
(this.events[event] || []).forEach(callback => callback(data));
}
}
// Expose globally
window.__EVENT_BUS__ = window.__EVENT_BUS__ || new EventBus();
// Usage in Products MFE
window.__EVENT_BUS__.publish('cart:add', { productId: '123', quantity: 1 });
// Usage in Cart MFE
window.__EVENT_BUS__.subscribe('cart:add', (item) => {
addToCart(item);
}); Shared State
// Shared state using RxJS
import { BehaviorSubject } from 'rxjs';
interface User {
id: string;
name: string;
email: string;
}
// Create shared store
const userSubject = new BehaviorSubject(null);
export const userStore = {
getUser: () => userSubject.getValue(),
setUser: (user: User) => userSubject.next(user),
subscribe: (callback: (user: User | null) => void) => {
const subscription = userSubject.subscribe(callback);
return () => subscription.unsubscribe();
}
};
// Expose as singleton
window.__USER_STORE__ = window.__USER_STORE__ || userStore; Routing Coordination
// Shell router that delegates to micro frontends
function ShellRouter() {
return (
}>
} />
}>
} />
}>
} />
);
}When to Use Micro Frontends
- Large teams needing autonomy
- Multiple products sharing components
- Gradual migration from legacy systems
- Different release cycles needed
Conclusion
Micro frontends solve organizational scaling, not technical problems. Use them when team independence and separate deployability justify the added complexity.