Solving Micro-Frontend Complexity: Module Federation with Vite, React 19 & TypeScript
Engineering scalable micro-frontends: dynamic remote module imports, sharing React 19 runtime dependencies, and isolating deployment blast radiuses with Vite.
By Uttam Thapa · · Frontend
⚡ Executive Summary (TL;DR)
Micro-frontends solve exactly one problem: several teams blocked behind one build and one release. Vite Module Federation splits a monolith into a
host shell and independently deployed remotes that load as ES modules at runtime, sharing React as a singleton. The cost is
a new failure mode — a remote can be unreachable — so every dynamic import needs an error boundary from the first day.
Figure 1: The host imports remote components at runtime — after they were built and deployed on someone else's schedule.
The Problem Is Organisational Before It Is Technical
Picture an enterprise application with thirty engineers across catalog, accounts and checkout. The code is well factored. The build is not.
🚨 Monolith friction, concretely
- Queued pipelines. A copy change on one page triggers an eighteen-minute build of everything.
- Coupled releases. Three teams ship together, so one team's regression blocks two teams' work.
- Blocked upgrades. Nobody can move to a new React version until everybody can.
- Shared blast radius. A crash in a recommendations widget takes down the checkout route.
💡 Check this before going further
Every item above is about coordination between teams. For a single team on one codebase, micro-frontends add runtime failure modes, version-skew
debugging and deployment coordination in exchange for independence you already have. Route-level code splitting gets you the bundle-size win with none of it.
The honest test: are separate teams blocked on each other's releases today? If not, this is not your problem.
Hosts and Remotes
Module Federation defines two roles. A remote builds its components into an ES module manifest published at its own URL. A host
declares which remotes it consumes and imports from them at runtime — meaning the host was built and deployed before the code it is now running existed.
1. Exposing a remote
// vite.config.ts — the product catalog micro-app
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import federation from '@originjs/vite-plugin-federation';
export default defineConfig({
plugins: [
react(),
federation({
name: 'productCatalog',
filename: 'remoteEntry.js',
exposes: {
'./ProductGrid': './src/components/ProductGrid.tsx',
},
// Singletons: two copies of React in one page breaks hooks and context.
shared: ['react', 'react-dom', 'framer-motion'],
}),
],
build: { target: 'esnext' },
});
The shared array is the load-bearing line. Without it each remote ships its own React, the page runs several copies, and hooks fail with errors that
point nowhere near the cause. Anything holding module-level state — React, the router, a state library, a styling runtime — must be shared.
2. Consuming it in the host
// CatalogView.tsx — the host shell
import React, { Suspense } from 'react';
import { RemoteBoundary } from './RemoteBoundary';
const RemoteProductGrid = React.lazy(() => import('productCatalog/ProductGrid'));
export const CatalogView: React.FC = () => (
<div className="p-6">
<h2>Enterprise Catalog</h2>
{/* The boundary is not optional: this import crosses the network at
runtime, so it can fail long after the host was built and tested. */}
<RemoteBoundary fallback={<CatalogUnavailable />}>
<Suspense fallback={<div className="h-64 animate-pulse bg-slate-100 rounded-xl" />}>
<RemoteProductGrid category="software" />
</Suspense>
</RemoteBoundary>
</div>
);
The Failure Modes You Are Buying
A federated import is a network dependency wearing the syntax of a local one. That mismatch is where most of the operational surprise lives.
| Failure |
How it appears |
Mitigation |
| Remote unreachable |
The host route crashes on import |
Error boundary with a real fallback UI |
| Version skew |
Remote expects a prop the host does not send |
Version the contract; treat props as a public API |
| Duplicate React |
"Invalid hook call" far from the cause |
Share every stateful dependency as a singleton |
| CSS collision |
One remote restyles another |
Scoped styles or CSS modules; no global resets in remotes |
| Cold remote load |
Visible delay on first navigation |
Prefetch the manifest on route hover |
Version skew is the one that scales worst. Because remotes deploy independently, a host can be running against a remote built weeks later. Props between host and
remote are a versioned public API, not an internal detail — change them the way you would change a REST contract, additively, with a deprecation window.
What You Actually Get
🚀
Deployment autonomy
A team ships its remote without rebuilding the host — the change that actually removes the queue.
🧠
One shared runtime
React is downloaded once and reused across every micro-app on the page.
🔓
Independent upgrades
Teams move on their own schedule — within the bounds the shared singletons allow.
✅ Key takeaways
- ✓Adopt for team independence, not code organisation. One team gets the same benefit from route splitting.
- ✓Share every stateful dependency as a singleton. Two Reacts on a page is the classic outage.
- ✓Wrap every remote in an error boundary. The import can fail long after the host shipped.
- ✓Treat props as a versioned public API. Independent deploys mean arbitrary version pairings.
- ✓Prefetch manifests on hover. Runtime loading is a real cost the user can feel.
- ✓Keep global CSS out of remotes. A reset in one micro-app restyles all of them.
For the backend equivalent of this trade-off — when to split, and when splitting costs more than it returns — see
system design basics for production applications.
Frequently asked questions
When are micro-frontends actually worth the complexity?
When independent deployment is the constraint — multiple teams blocked behind one build and one release. For a single team, they add runtime failure modes in exchange for benefits you already have.
How does Vite Module Federation share React between apps?
Declare React and React DOM as shared singletons in the federation config. The host loads one copy and remotes reuse it, which is essential because two React instances break hooks and context.
What happens if a remote micro-app fails to load?
Without protection, the host route crashes. Wrap every dynamic remote in an error boundary with a Suspense fallback so a degraded remote renders a placeholder instead of taking the page down.
Home · Projects · Blog · Services · Résumé · Contact