React Micro-Frontend Architecture: A Practical Guide
A practical guide to building React micro-frontends with Module Federation, routing, shared state, deployment, testing, SSR, performance, and security.
Image used for representation purposes only.
Why micro-frontends—and when to use them
Micro-frontends apply microservice principles to the browser. Instead of one large SPA, you compose multiple independently built, deployed, and owned frontends into a single experience.
Benefits:
- Independent deployability and faster release cycles
- Clear domain boundaries aligned to teams
- Technology isolation (opt-in, not chaos)
- Resilience: one part can fail without taking down the whole UI
Adopt them when your product, team count, and release cadence outgrow a single-repo SPA. If you have a small team, or most changes touch many pages at once, a well-structured monolith may be simpler.
Composition patterns
- Client-side composition (most common): A shell app dynamically loads remote UIs at runtime. Great for SPAs and dynamic routing.
- Server-side/edge composition: The server or edge gateway stitches HTML chunks. Ideal for SSR and first-paint performance.
- Build-time composition: Remotes compiled together at CI. Simpler caching, but weaker independence.
- Iframe isolation: Strong security and style isolation; heavier runtime and poor UX integration—use for untrusted or third-party content only.
Technology options for React micro-frontends
- Webpack Module Federation (MF): Runtime loading of separately deployed bundles; share libraries like React as singletons; production-proven.
- Import maps + native ESM: Framework-agnostic and simple, but lacks automatic shared dependency negotiation.
- Single-spa/orchestrators: Route-based composition across frameworks.
- Web Components: Interop-friendly leaf components; still need a composition strategy for code-splitting and shared deps.
Recommendation for most React SPAs: Module Federation for runtime composition plus conventional React tooling.
Reference architecture (React)
- Shell (host):
- Global routing and page frames (header, nav, auth boundary)
- Runtime loading of remotes (e.g., /catalog, /cart, /account)
- Cross-cutting concerns: auth tokens, feature flags, error boundary, telemetry
- Remotes (feature apps):
- Own build pipeline and repo
- Expose route-level components and optional widgets
- Share React/React DOM as singletons
- Shared libraries:
- Design system (tokens, components)
- Utilities (analytics client, HTTP, i18n)
- Avoid excessive sharing; keep versions compatible via semver
Walkthrough: React + Module Federation
The following shows a minimal host and two remotes (catalog and cart). Use React 18+, Webpack 5.
1) Host webpack config
// host/webpack.config.js
const { ModuleFederationPlugin } = require('webpack').container;
const deps = require('./package.json').dependencies;
module.exports = {
// ...entry, output, loaders
plugins: [
new ModuleFederationPlugin({
name: 'host',
remotes: {
// Local dev (static URL). For prod, see dynamic example below.
catalog: 'catalog@http://localhost:3001/remoteEntry.js',
cart: 'cart@http://localhost:3002/remoteEntry.js',
},
shared: {
react: { singleton: true, requiredVersion: deps.react },
'react-dom': { singleton: true, requiredVersion: deps['react-dom'] },
'react-router-dom': { singleton: true },
},
}),
],
};
2) Remote webpack config
// cart/webpack.config.js
const { ModuleFederationPlugin } = require('webpack').container;
const deps = require('./package.json').dependencies;
module.exports = {
// ...entry, output, loaders
plugins: [
new ModuleFederationPlugin({
name: 'cart',
filename: 'remoteEntry.js',
exposes: {
'./Routes': './src/routes',
'./MiniCart': './src/components/MiniCart',
},
shared: {
react: { singleton: true, requiredVersion: deps.react },
'react-dom': { singleton: true, requiredVersion: deps['react-dom'] },
},
}),
],
};
3) Dynamic remotes for production
Keep remote URLs out of the bundle so you can deploy new versions without rebuilding the host.
// host/webpack.config.js (dynamic remotes)
new ModuleFederationPlugin({
name: 'host',
remotes: {
catalog: `promise new Promise(resolve => {
const url = window.appConfig.remotes.catalog; // set at runtime
const s = document.createElement('script');
s.src = url;
s.onload = () => {
const proxy = {
get: req => window.catalog.get(req),
init: arg => { try { return window.catalog.init(arg); } catch(e) { /* already initialized */ } }
};
resolve(proxy);
};
document.head.appendChild(s);
})`,
},
shared: { react: { singleton: true }, 'react-dom': { singleton: true } },
});
<!-- host/index.html: runtime config injected by your CDN or server -->
<script>
window.appConfig = {
remotes: {
catalog: 'https://cdn.example.com/catalog/v2.4.1/remoteEntry.js',
cart: 'https://cdn.example.com/cart/v3.1.0/remoteEntry.js'
}
};
</script>
4) Bootstrap and routing in the host
// host/src/App.tsx
import React, { Suspense, lazy } from 'react';
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
const CatalogRoutes = lazy(() => import('catalog/Routes'));
const CartRoutes = lazy(() => import('cart/Routes'));
export default function App() {
return (
<BrowserRouter>
<Suspense fallback={<div>Loading…</div>}>
<Routes>
<Route path="/catalog/*" element={<CatalogRoutes />} />
<Route path="/cart/*" element={<CartRoutes />} />
<Route path="/" element={<Navigate to="/catalog" replace />} />
<Route path="*" element={<div>404</div>} />
</Routes>
</Suspense>
</BrowserRouter>
);
}
5) TypeScript declarations
// host/src/types/federation.d.ts
declare module 'catalog/Routes';
declare module 'cart/Routes';
// If you reference window.cart, also declare it:
declare global { interface Window { cart: any; catalog: any } }
6) Cross-app communication
Prefer explicit interfaces. A few safe patterns:
- URL and routing: encode state in URLs, not globals
- Props/contracts: expose leaf widgets with typed props
- Event bridge: use DOM CustomEvents for decoupled signaling
// In catalog when a product is added to cart
window.dispatchEvent(new CustomEvent('cart:add', { detail: { sku: 'SKU123', qty: 1 } }));
// In cart remote (mounted once)
useEffect(() => {
const onAdd = (e: any) => cartApi.add(e.detail.sku, e.detail.qty);
window.addEventListener('cart:add', onAdd as any);
return () => window.removeEventListener('cart:add', onAdd as any);
}, []);
For shared state libraries (e.g., Redux, Zustand), avoid coupling: either scope each remote’s store or expose a thin API from the shell.
7) Shared design system
Publish your design system as a versioned package. Make React a peerDependency of the design system to avoid multiple Reacts. Example package.json extract:
{
"name": "@acme/design-system",
"version": "1.8.0",
"peerDependencies": { "react": ">=18", "react-dom": ">=18" }
}
Deployment and versioning
- Independent pipelines: each remote builds, tests, and publishes its assets to a CDN.
- Filenames: content-hashed assets for long-term caching; remoteEntry.js can be versioned by path (e.g., /v3.1.0/remoteEntry.js) to enable safe rollbacks.
- Manifest: the shell reads a runtime manifest (window.appConfig or JSON endpoint) to resolve current remote URLs per environment.
- Backward compatibility: treat exposed modules as public APIs; follow semver and add deprecation windows.
Rollback strategy:
- Update the manifest to point to the previous known-good remote version; the shell and other remotes remain untouched.
Testing strategy
- Unit tests per remote
- Contract tests for exposed modules and widget props
- Visual regression on key routes and shared components
- Integration smoke tests in the shell against production-like remotes
- Synthetic monitoring: load each remoteEntry periodically and check basic render paths
Observability and error handling
- Tag telemetry with app and version (e.g., cart@3.1.0 )
- Propagate correlation IDs (traceparent) in fetch/XHR across remotes
- Global error boundary in the shell; local error boundaries in remotes
- Soft-fail UI: if a remote fails to load, show a fallback card with retry and status info
// Example: resilient loader
const RemoteMiniCart = lazy(() => import('cart/MiniCart'));
<ErrorBoundary fallback={<MiniCartFallback />}>
<Suspense fallback={<MiniCartSkeleton />}>
<RemoteMiniCart />
</Suspense>
</ErrorBoundary>
Performance and UX
- Keep React singleton: prevents duplicate React copies and reduces memory
- Preload critical remotes with for first-route remotes
- Code-split within remotes, not only at the remote boundary
- Share judiciously: oversharing inflates initial vendor bundles; prefer per-remote deps unless there’s a clear win
- CSS isolation: use CSS Modules or CSS-in-JS with hashed class names; avoid global leakage
- Image and font strategy remains per-remote but follow shared guidelines (formats, cache headers)
Security considerations
- Content Security Policy (CSP): restrict script and connect sources to expected CDNs
- Subresource Integrity (SRI): where feasible, pin integrity for remoteEntry or protect via signed URLs
- Escape and sanitize: treat cross-remote data like external input
- Iframe sandboxing: if you host untrusted remotes, sandbox them in iframes and communicate via postMessage
- Dependency hygiene: track vulnerabilities per remote; small remotes still carry supply-chain risk
SSR and streaming
- Server/edge composition improves first paint and SEO. Two approaches:
- Shell SSR that preloads remoteEntry and renders remote routes via MF on the server
- Fragment composition: each remote renders HTML on the server; the shell stitches and streams
- Keep React versions aligned across SSR boundaries; ensure consistent design-system CSS order
- Hydration: expose stable component IDs and avoid time-based randomness during SSR
Migration plan (from a monolith)
- Identify seams by domain (e.g., Catalog, Cart, Account)
- Extract the simplest route as the first remote to validate infra
- Introduce the shell and a single dynamic remote with a feature flag
- Move shared concerns into a design system and utilities package
- Migrate routes gradually (strangler pattern); keep URLs stable
- Establish versioning, manifest, and rollback routine before scaling to more teams
Team topology and governance
- One team per domain/remote; the shell team acts as platform
- A small cross-team council maintains contracts, design tokens, and release guidelines
- Define SLAs for remote availability and load budget (kb, async chunks, TTI targets)
Common anti-patterns
- Over-fragmentation: dozens of tiny remotes increase latency and cognitive load
- Hidden coupling: sharing mutable singletons for cross-remote state
- Version drift: React or design-system versions diverge and break SSR/hydration
- Orchestrator fatigue: reinventing deployment/manifest logic in each team—centralize it once
Production readiness checklist
- React and React DOM shared as singletons with compatible versions
- Remote URLs resolved at runtime via environment manifest
- Error boundaries + graceful fallbacks for each remote zone
- Telemetry includes app name and version; correlation IDs propagate
- Contract tests for exposed modules and public props
- Cache strategy defined (hashing, TTLs, purge process)
- Security: CSP, dependency scanning, and sanitized inputs
Final thoughts
Micro-frontends are an organizational architecture as much as a technical one. Start with strong domain boundaries, keep interfaces small and explicit, and build robust platform primitives—runtime manifests, observability, and quality gates. With these in place, React micro-frontends can scale teams and codebases without sacrificing user experience.
Related Posts
Mastering React Virtualized Lists: Performance Patterns and Tuning Guide
Build fast, memory‑efficient React virtualized lists with tuning tips for overscan, variable heights, a11y, and profiling—plus practical code examples.
Framer Motion for React: A Practical Tutorial with Patterns and Pitfalls
Hands-on Framer Motion tutorial for React: setup, key APIs, gestures, layout, AnimatePresence, scroll, SVG, performance, TypeScript, testing.
React useMemo and useCallback: A Practical Optimization Guide
Practical guide to React’s useMemo and useCallback: when to use them, pitfalls to avoid, patterns, and profiling tips for faster apps.