Building React Micro-Frontends with Vite + Module Federation and Independent CI/CD Pipelines

As engineering teams scale, monolithic frontend applications face severe bottlenecks: exponentially growing build times, frequent code merge conflicts, and high blast radiuses where a single broken release crashes the entire web application. To address these issues, Micro-Frontends adapt the principles of microservices architecture to client-side applications.
While Webpack 5 pioneered Module Federation, modern developers leverage Vite alongside Module Federation 2.0 (@originjs/plugin-module-federation or @module-federation/vite) to achieve fast HMR, ESM-native builds, and runtime module sharing.
This article details how to architect a React Micro-Frontend with Vite, manage shared singleton dependencies, handle runtime errors, and deploy independent pipelines via Cloudflare Pages and GitHub Actions.
Key Takeaways
- Core Value of Module Federation: Remote modules are fetched at runtime instead of build time, allowing micro-apps to be built and deployed completely independently without rebuilding the Shell host.
- Vite Runtime Sharing: Critical common libraries like
reactandreact-domare declared assharedsingletons to avoid duplicate bundle downloads and memory footprint overhead.- Independent CI/CD: Remote application updates require zero modifications to the Host application. Updating the remote
remoteEntry.jsscript allows instant zero-downtime and canary deployments.- Error Isolation: Placing React
ErrorBoundaryboundaries around dynamically loaded Remote components prevents isolated micro-app failures from breaking the overall Host Shell.
1. Micro-Frontend Architecture Overview
The system consists of a Host (Shell) App providing global routing, authentication, and layout, and multiple Remote Apps hosting domain-specific micro-applications.
+------------------------------------------------------------------+
| Host App (Shell) |
| - Layout Header / Sidebar |
| - Auth State / Router Core |
| |
| +--------------------------+ +--------------------------+ |
| | Payment Remote (App 1) | | Product Remote (App 2) | |
| | https://pay.domain.com | | https://prod.domain.com | |
| | /remoteEntry.js | | /remoteEntry.js | |
| +--------------------------+ +--------------------------+ |
+------------------------------------------------------------------+
2. Configuring Remote App (vite.config.ts)
// remote-payment/vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import federation from '@originjs/plugin-module-federation';
export default defineConfig({
plugins: [
react(),
federation({
name: 'paymentApp',
filename: 'remoteEntry.js',
exposes: {
'./CheckoutForm': './src/components/CheckoutForm.tsx',
},
shared: {
react: { singleton: true, requiredVersion: '^18.0.0 || ^19.0.0' },
'react-dom': { singleton: true, requiredVersion: '^18.0.0 || ^19.0.0' },
},
}),
],
build: {
target: 'esnext',
},
});
3. Host App Integration with Error Boundaries
// host-shell/src/pages/CheckoutPage.tsx
import React, { Suspense } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
const RemoteCheckoutForm = React.lazy(() => import('paymentApp/CheckoutForm'));
export function CheckoutPage() {
return (
<div className="max-w-xl mx-auto p-6 space-y-4">
<h1 className="text-2xl font-bold">Checkout</h1>
<ErrorBoundary fallback={<div className="text-red-500">Failed to load payment module.</div>}>
<Suspense fallback={<div>Loading remote form...</div>}>
<RemoteCheckoutForm orderId="ORD-2026-9901" amount={45000} />
</Suspense>
</ErrorBoundary>
</div>
);
}
4. Architecture Comparison
| Feature | Monolith | Iframe Strategy | Vite Module Federation |
|---|---|---|---|
| Independent Build | No | Yes | Yes |
| React Singleton | Guaranteed | Broken (Dual Load) | Guaranteed via shared |
| Initial Load | Heavy Monolith Bundle | Slower Iframe Overhead | Lazy-loaded on demand |
| CI/CD Build Time | 10-20 min | 2-3 min | < 1 min per Remote |