Architecture
Micro-Frontends
Breaking monolithic frontends into independent, deployable applications
Micro-Frontends
Micro-frontends extend microservices concepts to the frontend, allowing teams to build and deploy features independently.
When to Use
Good Fit
- Large teams working on different features
- Features with different release cycles
- Gradual migration from legacy systems
- Different tech stack requirements per team
Not Recommended
- Small teams or simple applications
- Tight coupling between features
- Shared state requirements
- Performance-critical applications
Architecture Approaches
Build-Time Integration
// Package-based micro-frontends
// Each micro-frontend is an npm package
// package.json
{
"dependencies": {
"@myorg/header": "^1.0.0",
"@myorg/dashboard": "^2.0.0",
"@myorg/settings": "^1.5.0"
}
}
// App.tsx
import { Header } from '@myorg/header';
import { Dashboard } from '@myorg/dashboard';
import { Settings } from '@myorg/settings';
function App() {
return (
<>
<Header />
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</>
);
}Runtime Integration (Module Federation)
// webpack.config.js (Host Application)
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'host',
remotes: {
dashboard: 'dashboard@http://localhost:3001/remoteEntry.js',
settings: 'settings@http://localhost:3002/remoteEntry.js',
},
shared: {
react: { singleton: true, requiredVersion: '^18.0.0' },
'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
},
}),
],
};
// webpack.config.js (Dashboard Micro-frontend)
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'dashboard',
filename: 'remoteEntry.js',
exposes: {
'./Dashboard': './src/Dashboard',
},
shared: {
react: { singleton: true },
'react-dom': { singleton: true },
},
}),
],
};
// Host App - Dynamic import
const Dashboard = lazy(() => import('dashboard/Dashboard'));
const Settings = lazy(() => import('settings/Settings'));
function App() {
return (
<Suspense fallback={<Loading />}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
);
}Vite Module Federation
// vite.config.ts (Host)
import federation from '@originjs/vite-plugin-federation';
export default defineConfig({
plugins: [
react(),
federation({
name: 'host',
remotes: {
dashboard: 'http://localhost:3001/assets/remoteEntry.js',
},
shared: ['react', 'react-dom'],
}),
],
});
// vite.config.ts (Remote)
export default defineConfig({
plugins: [
react(),
federation({
name: 'dashboard',
filename: 'remoteEntry.js',
exposes: {
'./Dashboard': './src/Dashboard.tsx',
},
shared: ['react', 'react-dom'],
}),
],
});iframe Integration
// Simple but isolated approach
function MicroFrontendIframe({ url, title }: Props) {
const [height, setHeight] = useState(600);
useEffect(() => {
const handleMessage = (event: MessageEvent) => {
if (event.data.type === 'resize') {
setHeight(event.data.height);
}
};
window.addEventListener('message', handleMessage);
return () => window.removeEventListener('message', handleMessage);
}, []);
return (
<iframe
src={url}
title={title}
style={{ width: '100%', height, border: 'none' }}
/>
);
}
// Communication via postMessage
// Child (micro-frontend)
window.parent.postMessage({ type: 'resize', height: document.body.scrollHeight }, '*');
window.parent.postMessage({ type: 'navigate', path: '/dashboard' }, '*');
// Parent (shell)
window.addEventListener('message', (event) => {
if (event.data.type === 'navigate') {
router.push(event.data.path);
}
});Web Components
// Micro-frontend as Web Component
class DashboardElement extends HTMLElement {
connectedCallback() {
const shadow = this.attachShadow({ mode: 'open' });
const root = document.createElement('div');
shadow.appendChild(root);
// Mount React app
ReactDOM.createRoot(root).render(<Dashboard />);
}
disconnectedCallback() {
// Cleanup
}
}
customElements.define('mf-dashboard', DashboardElement);
// Usage in any framework
<mf-dashboard user-id="123"></mf-dashboard>Communication Patterns
Custom Events
// Dispatch from micro-frontend
function dispatchMicroFrontendEvent(type: string, detail: any) {
window.dispatchEvent(new CustomEvent(`mf:${type}`, { detail }));
}
dispatchMicroFrontendEvent('user:updated', { userId: '123', name: 'John' });
// Listen in shell or other micro-frontend
window.addEventListener('mf:user:updated', ((event: CustomEvent) => {
console.log('User updated:', event.detail);
}) as EventListener);
// React hook
function useMicroFrontendEvent<T>(type: string, handler: (data: T) => void) {
useEffect(() => {
const listener = ((event: CustomEvent<T>) => {
handler(event.detail);
}) as EventListener;
window.addEventListener(`mf:${type}`, listener);
return () => window.removeEventListener(`mf:${type}`, listener);
}, [type, handler]);
}Shared State
// Shared state container
class SharedState {
private state: Record<string, any> = {};
private listeners = new Map<string, Set<(value: any) => void>>();
get<T>(key: string): T | undefined {
return this.state[key];
}
set<T>(key: string, value: T): void {
this.state[key] = value;
this.listeners.get(key)?.forEach(listener => listener(value));
}
subscribe<T>(key: string, listener: (value: T) => void): () => void {
if (!this.listeners.has(key)) {
this.listeners.set(key, new Set());
}
this.listeners.get(key)!.add(listener);
return () => this.listeners.get(key)?.delete(listener);
}
}
// Expose globally
window.__SHARED_STATE__ = new SharedState();
// React hook
function useSharedState<T>(key: string): [T | undefined, (value: T) => void] {
const [value, setValue] = useState<T | undefined>(() =>
window.__SHARED_STATE__.get(key)
);
useEffect(() => {
return window.__SHARED_STATE__.subscribe(key, setValue);
}, [key]);
const setSharedValue = useCallback((newValue: T) => {
window.__SHARED_STATE__.set(key, newValue);
}, [key]);
return [value, setSharedValue];
}Shared Dependencies
Dependency Strategy
// Module Federation shared config
{
shared: {
// Singleton - only one version loaded
react: {
singleton: true,
requiredVersion: '^18.0.0',
},
'react-dom': {
singleton: true,
requiredVersion: '^18.0.0',
},
// Shared but allow multiple versions
lodash: {
singleton: false,
},
// Eager loading for shell dependencies
'@myorg/design-system': {
singleton: true,
eager: true,
},
},
}Externals CDN
<!-- Shell HTML -->
<script src="https://cdn.example.com/react@18.2.0/umd/react.production.min.js"></script>
<script src="https://cdn.example.com/react-dom@18.2.0/umd/react-dom.production.min.js"></script>// webpack.config.js
module.exports = {
externals: {
react: 'React',
'react-dom': 'ReactDOM',
},
};Routing
Shell-Based Routing
// Shell handles top-level routing
function Shell() {
return (
<BrowserRouter>
<Header />
<Suspense fallback={<Loading />}>
<Routes>
<Route path="/dashboard/*" element={<DashboardMicroFrontend />} />
<Route path="/settings/*" element={<SettingsMicroFrontend />} />
</Routes>
</Suspense>
</BrowserRouter>
);
}
// Micro-frontend handles its own sub-routes
function DashboardMicroFrontend() {
return (
<Routes>
<Route path="/" element={<DashboardHome />} />
<Route path="/analytics" element={<Analytics />} />
<Route path="/reports" element={<Reports />} />
</Routes>
);
}Memory Router for Micro-frontends
// Micro-frontend receives basePath from shell
function MicroFrontendApp({ basePath }: { basePath: string }) {
return (
<MemoryRouter initialEntries={[basePath]}>
<Routes>
<Route path="/feature/*" element={<FeatureRoutes />} />
</Routes>
</MemoryRouter>
);
}Deployment
Independent Deployment Pipeline
# .github/workflows/deploy-dashboard.yml
name: Deploy Dashboard Micro-frontend
on:
push:
paths:
- 'apps/dashboard/**'
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build
run: |
cd apps/dashboard
npm ci
npm run build
- name: Deploy to CDN
run: |
aws s3 sync dist/ s3://mf-dashboard/
aws cloudfront create-invalidation --distribution-id ${{ secrets.CF_DIST_ID }} --paths "/*"Version Management
// Version manifest
{
"dashboard": {
"url": "https://cdn.example.com/dashboard/v2.1.0/remoteEntry.js",
"integrity": "sha384-...",
"version": "2.1.0"
},
"settings": {
"url": "https://cdn.example.com/settings/v1.5.0/remoteEntry.js",
"integrity": "sha384-...",
"version": "1.5.0"
}
}
// Shell loads from manifest
async function loadMicroFrontends() {
const manifest = await fetch('/manifest.json').then(r => r.json());
for (const [name, config] of Object.entries(manifest)) {
await loadRemote(config.url, { integrity: config.integrity });
}
}Best Practices
Micro-Frontend Guidelines
- Define clear boundaries between micro-frontends
- Minimize shared state - prefer events
- Use consistent design system across apps
- Version shared dependencies carefully
- Implement health checks and fallbacks
- Monitor performance of remote loading
- Test integration between micro-frontends
- Document communication contracts