Micro-Frontend Patterns
When to Use MFEs
Use when: independent teams, independent deployments, different tech stacks, large-scale apps. Avoid when: small team, shared release cycle, high communication overhead not justified.
Module Federation (Webpack 5)
// host/webpack.config.js
const { ModuleFederationPlugin } = require('webpack').container;
module.exports = {
plugins: [new ModuleFederationPlugin({
name: 'host',
remotes: {
auth: 'auth@http://localhost:3001/remoteEntry.js',
dashboard: 'dashboard@http://localhost:3002/remoteEntry.js',
},
shared: {
react: { singleton: true, requiredVersion: '^18' },
'react-dom': { singleton: true, requiredVersion: '^18' },
},
})]
};
// host/src/App.jsx
const LoginForm = React.lazy(() => import('auth/LoginForm'));
const Dashboard = React.lazy(() => import('dashboard/Dashboard'));
// auth/webpack.config.js — Remote
module.exports = {
plugins: [new ModuleFederationPlugin({
name: 'auth',
filename: 'remoteEntry.js',
exposes: {
'./LoginForm': './src/components/LoginForm',
'./AuthProvider': './src/providers/AuthProvider',
},
shared: { react: { singleton: true }, 'react-dom': { singleton: true } },
})]
};
Vite Module Federation
// vite.config.ts
import federation from '@originjs/vite-plugin-federation';
// Host
export default defineConfig({
plugins: [federation({
remotes: { mfe_nav: 'http://localhost:3001/assets/remoteEntry.js' },
shared: ['react', 'react-dom'],
})]
});
// Remote (must build with target: 'esnext')
export default defineConfig({
plugins: [federation({
name: 'mfe_nav',
filename: 'remoteEntry.js',
exposes: { './NavBar': './src/NavBar' },
shared: ['react', 'react-dom'],
})],
build: { target: 'esnext', minify: false }
});
Single-SPA
// root-config.js
import { registerApplication, start } from 'single-spa';
registerApplication({
name: '@myorg/navbar',
app: () => System.import('@myorg/navbar'),
activeWhen: () => true,
});
registerApplication({
name: '@myorg/dashboard',
app: () => System.import('@myorg/dashboard'),
activeWhen: ['/dashboard'],
});
start({ urlRerouteOnly: true });
Web Component Integration
// Any framework exports a web component
// React MFE exposes as custom element
import { createRoot } from 'react-dom/client';
import { Dashboard } from './Dashboard';
class DashboardElement extends HTMLElement {
#root;
connectedCallback() {
this.#root = createRoot(this);
this.#root.render(<Dashboard config={this.#getConfig()} />);
}
disconnectedCallback() { this.#root.unmount(); }
#getConfig() {
return { theme: this.getAttribute('theme') ?? 'light' };
}
}
customElements.define('mfe-dashboard', DashboardElement);
// Host (any framework) uses it:
// <mfe-dashboard theme="dark"></mfe-dashboard>
Cross-MFE Communication
// Use a shared event bus (CustomEvents on window)
// auth MFE emits:
window.dispatchEvent(new CustomEvent('mfe:auth:login', {
detail: { userId: '123', token: 'abc' }
}));
// dashboard MFE listens:
window.addEventListener('mfe:auth:login', ({ detail }) => {
store.setUser(detail);
});
// Or use a shared store via singleton
// shared-store/index.js (exposed via module federation)
import { create } from 'zustand';
export const useSharedStore = create(set => ({
user: null, setUser: user => set({ user }),
}));
Deployment Pattern
CDN / Nginx (host)
├── / → host app (index.html + hashed JS)
├── /auth/ → auth MFE (remoteEntry.js + chunks)
└── /dashboard/ → dashboard MFE (remoteEntry.js + chunks)
# Each MFE deployed independently via own CI/CD pipeline
# remoteEntry.js is the only stable URL — all other files are hashed
Shared Design System
// design-system remote
exposes: {
'./Button': './src/Button',
'./tokens': './src/tokens',
'./theme': './src/ThemeProvider',
}
// All MFEs import from design-system remote
// Single source of truth for UI components