Micro-Frontends: Module Federation, Independent Deployment, and Shared State
Implement micro-frontends with Webpack Module Federation — independent deployment, shared dependencies, cross-app state management, and when micro-frontends are
Micro-Frontends: Module Federation, Independent Deployment, and Shared State
Micro-frontends apply the microservices principle to the frontend: instead of one large application, you have multiple independently deployable frontend apps that compose into a single user experience. A team can ship the checkout widget without touching the product catalog, and the navigation bar team can release independently of everyone else.
This solves real problems at large organizations. It also introduces significant complexity that makes no sense for smaller teams. Start with the tradeoffs.
When Micro-Frontends Make Sense
| Signal | Details |
|---|---|
| Multiple teams on one frontend | 5+ teams merging PRs to the same repo causes constant conflicts |
| Different release cadences | Team A ships daily; team B ships monthly — they shouldn't block each other |
| Different tech stacks | Legacy Angular + new React — Module Federation lets them coexist |
| Organizational autonomy | Business units want independent ownership and deployment |
When they don't make sense:
- Teams < 20 engineers — shared monorepo with Nx/Turborepo is simpler
- Greenfield product — you don't know where the seams are yet
- Single team — independent deployment doesn't help if one team owns everything
- Tight shared state — frequent cross-app state updates negate isolation benefits
Module Federation: The Core Mechanism
Webpack 5's Module Federation allows a JavaScript bundle to expose modules that other bundles consume at runtime — without bundling them at build time.
Shell App (host) Product Team (remote) Cart Team (remote)
├── index.html ├── remoteEntry.js ├── remoteEntry.js
├── shell bundle ├── ProductList component ├── CartWidget component
└── loads at runtime ──→ └── exposes ProductList └── exposes CartWidget
The shell loads remote bundles at runtime — updates to ProductList deploy without rebuilding the shell.
🌐 Looking for a Dev Team That Actually Delivers?
Most agencies sell you a project manager and assign juniors. Viprasol is different — senior engineers only, direct Slack access, and a 5.0★ Upwork record across 100+ projects.
- React, Next.js, Node.js, TypeScript — production-grade stack
- Fixed-price contracts — no surprise invoices
- Full source code ownership from day one
- 90-day post-launch support included
Setup: Remote App (Exposes Components)
// product-app/webpack.config.js
const { ModuleFederationPlugin } = require('webpack').container;
const deps = require('./package.json').dependencies;
module.exports = {
mode: 'development',
devServer: { port: 3001 },
plugins: [
new ModuleFederationPlugin({
name: 'productApp', // Unique name for this remote
filename: 'remoteEntry.js', // Entry point the host loads
// Expose these modules to other apps
exposes: {
'./ProductList': './src/components/ProductList',
'./ProductDetail': './src/components/ProductDetail',
},
// Share dependencies — prevents loading React twice
shared: {
react: {
singleton: true, // Only one instance across all apps
requiredVersion: deps.react,
},
'react-dom': {
singleton: true,
requiredVersion: deps['react-dom'],
},
},
}),
],
};
// product-app/src/components/ProductList.tsx
// This component will be consumed by the shell
interface Product {
id: string;
name: string;
price: number;
}
interface ProductListProps {
onAddToCart: (productId: string) => void;
}
export default function ProductList({ onAddToCart }: ProductListProps) {
const [products, setProducts] = useState<Product[]>([]);
useEffect(() => {
fetch('/api/products').then(r => r.json()).then(setProducts);
}, []);
return (
<div className="product-grid">
{products.map(product => (
<div key={product.id} className="product-card">
<h3>{product.name}</h3>
<p>${(product.price / 100).toFixed(2)}</p>
<button onClick={() => onAddToCart(product.id)}>
Add to Cart
</button>
</div>
))}
</div>
);
}
Setup: Shell App (Host — Loads Remotes)
// shell/webpack.config.js
const { ModuleFederationPlugin } = require('webpack').container;
const deps = require('./package.json').dependencies;
module.exports = {
mode: 'development',
devServer: { port: 3000 },
plugins: [
new ModuleFederationPlugin({
name: 'shell',
// Define remote apps and where to load them from
remotes: {
productApp: 'productApp@http://localhost:3001/remoteEntry.js',
cartApp: 'cartApp@http://localhost:3002/remoteEntry.js',
},
shared: {
react: { singleton: true, requiredVersion: deps.react },
'react-dom': { singleton: true, requiredVersion: deps['react-dom'] },
},
}),
],
};
// shell/src/App.tsx
import React, { Suspense, lazy } from 'react';
// Lazy load remote components — loaded at runtime, not build time
const ProductList = lazy(() => import('productApp/ProductList'));
const CartWidget = lazy(() => import('cartApp/CartWidget'));
export default function App() {
const handleAddToCart = (productId: string) => {
// Cross-app communication via custom events (see shared state section)
window.dispatchEvent(new CustomEvent('cart:add', { detail: { productId } }));
};
return (
<div className="app">
<header>
<h1>My Store</h1>
<Suspense fallback={<div>Loading cart...</div>}>
<CartWidget />
</Suspense>
</header>
<main>
<Suspense fallback={<div>Loading products...</div>}>
<ProductList onAddToCart={handleAddToCart} />
</Suspense>
</main>
</div>
);
}
🚀 Senior Engineers. No Junior Handoffs. Ever.
You get the senior developer, not a project manager who relays your requirements to someone you never meet. Every Viprasol project has a senior lead from kickoff to launch.
- MVPs in 4–8 weeks, full platforms in 3–5 months
- Lighthouse 90+ performance scores standard
- Works across US, UK, AU timezones
- Free 30-min architecture review, no commitment
Cross-App State Management
Sharing state between independently deployed apps is the hardest micro-frontend problem. Three patterns:
1. Custom Browser Events (Simple, Decoupled)
// Publish (from product app)
window.dispatchEvent(new CustomEvent('cart:add', {
detail: { productId: 'prod-123', quantity: 1 },
bubbles: true,
}));
// Subscribe (in cart app)
useEffect(() => {
const handler = (e: CustomEvent) => {
addToCart(e.detail.productId, e.detail.quantity);
};
window.addEventListener('cart:add', handler as EventListener);
return () => window.removeEventListener('cart:add', handler as EventListener);
}, []);
Good for: simple, fire-and-forget events. Bad for: reading shared state (you can't query event history).
2. Shared State Module (via Module Federation)
// shared-state/src/cartStore.ts — exposed as a shared module
import { create } from 'zustand';
interface CartStore {
items: { productId: string; quantity: number }[];
addItem: (productId: string, quantity?: number) => void;
removeItem: (productId: string) => void;
total: number;
}
// Singleton store — Module Federation + singleton:true ensures
// all apps use the same store instance
export const useCartStore = create<CartStore>((set, get) => ({
items: [],
addItem: (productId, quantity = 1) => set(state => ({
items: [...state.items, { productId, quantity }],
})),
removeItem: (productId) => set(state => ({
items: state.items.filter(i => i.productId !== productId),
})),
get total() { return get().items.reduce((sum, i) => sum + i.quantity, 0); },
}));
// webpack.config.js — expose shared state
exposes: {
'./cartStore': './src/cartStore',
},
shared: {
zustand: { singleton: true },
}
3. URL / Query String (For Navigation State)
For state that should survive navigation: put it in the URL. Both apps can read it independently.
Dynamic Remote URLs (Production)
In production, remote URLs change per environment. Don't hardcode them:
// shell/src/bootstrap.ts
async function loadRemotes() {
// Fetch remote URLs from a config service or environment
const config = await fetch('/api/mfe-config').then(r => r.json());
// config = { productApp: 'https://products.yourapp.com/remoteEntry.js', ... }
// Dynamically load remotes
for (const [name, url] of Object.entries(config)) {
await loadRemoteEntry(name, url as string);
}
}
async function loadRemoteEntry(name: string, url: string): Promise<void> {
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = url;
script.onload = () => resolve();
script.onerror = () => reject(new Error(`Failed to load remote: ${name}`));
document.head.appendChild(script);
});
}
Error Boundaries for Remote Failures
Remote apps can fail to load (deploy error, CDN issue). Always wrap in error boundaries:
// shell/src/RemoteErrorBoundary.tsx
import React, { Component, ReactNode } from 'react';
interface Props {
children: ReactNode;
fallback: ReactNode;
remoteName: string;
}
interface State { hasError: boolean; error?: Error }
export class RemoteErrorBoundary extends Component<Props, State> {
state: State = { hasError: false };
static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}
componentDidCatch(error: Error) {
console.error(`Remote "${this.props.remoteName}" failed to load:`, error);
// Report to your error tracking (Sentry, Datadog)
}
render() {
if (this.state.hasError) {
return this.props.fallback;
}
return this.props.children;
}
}
// Usage
<RemoteErrorBoundary
remoteName="productApp"
fallback={<div>Product catalog temporarily unavailable</div>}
>
<Suspense fallback={<Spinner />}>
<ProductList onAddToCart={handleAddToCart} />
</Suspense>
</RemoteErrorBoundary>
Module Federation vs Alternatives
| Approach | Setup Complexity | Runtime Overhead | Best For |
|---|---|---|---|
| Module Federation | High | Low (native browser loading) | Large organizations, different release cadences |
| iframes | Low | Medium | True isolation, legacy apps |
| Nx/Turborepo monorepo | Low | None | Single team, shared deploys acceptable |
| Web Components | Medium | Low | Framework-agnostic sharing |
| npm packages | Low | None (build-time) | Shared UI libraries, not independent deployment |
For most teams: Nx monorepo delivers 80% of the organizational benefits (independent ownership, parallel builds, clear module boundaries) without runtime complexity. Use Module Federation only when independent deployment is a hard requirement.
Working With Viprasol
We design and implement micro-frontend architectures for large engineering organizations — Module Federation configuration, shared state patterns, CI/CD pipelines for independent deployment, and migration strategies from monolithic frontends.
→ Talk to our team about frontend architecture for growing engineering organizations.
See Also
- Monorepo Tools — Nx and Turborepo as simpler alternatives
- React State Management — state patterns within a single app
- Feature Flags — coordinating releases across micro-frontend teams
- Web Performance Optimization — performance implications of micro-frontends
- Web Development Services — frontend architecture and engineering
About the Author
Viprasol Tech Team
Custom Software Development Specialists
The Viprasol Tech team specialises in algorithmic trading software, AI agent systems, and SaaS development. With 100+ projects delivered across MT4/MT5 EAs, fintech platforms, and production AI systems, the team brings deep technical experience to every engagement. Based in India, serving clients globally.
Need a Modern Web Application?
From landing pages to complex SaaS platforms — we build it all with Next.js and React.
Free consultation • No commitment • Response within 24 hours
Need a custom web application built?
We build React and Next.js web applications with Lighthouse ≥90 scores, mobile-first design, and full source code ownership. Senior engineers only — from architecture through deployment.