Mastering Module Federation React in 2026
You're probably dealing with one of two situations.
Either your React app grew into a frontend monolith that nobody wants to touch on Friday afternoon, or your company split feature ownership across teams and the release process turned into a negotiation. Builds feel heavier than they should. Shared dependencies get brittle. A small UI change can trigger a full regression cycle because everything ships together.
That's where Module Federation starts to matter. Not because it's trendy, and not because micro-frontends automatically make architecture better. It matters because it gives React teams a way to split a large application into independently deployed parts without forcing a full rewrite. Used well, it reduces coordination pain. Used badly, it replaces one big mess with several smaller ones that fail in more creative ways.
Breaking Up the Monolith without Breaking the App
A monolithic React app usually becomes painful in predictable ways. One repo holds every feature, every route, and every dependency decision. Teams step on each other's changes. CI slows down. Deployments become all-or-nothing. Even if the codebase is clean, the delivery model starts working against the team.
The issue isn't file size. It's coupling.
When checkout, account settings, search, and admin tooling all ship as one frontend, every team inherits the blast radius of every other team. That's why many frontend modernization efforts stall. People know the current setup hurts, but a full rewrite is too risky, too expensive, and too hard to sequence around active product work. That's also why a phased decomposition approach usually works better than a rewrite-first strategy, especially in organizations already dealing with aging systems and incremental platform changes. The same pattern shows up in broader legacy system modernization strategies.
Start with seams, not screens
The first mistake I see is splitting by UI layout. Teams pull out a navbar, a dashboard card, or a modal library and call it a micro-frontend plan. That rarely helps. Shared layout components don't create meaningful ownership boundaries.
A better starting point is business capability:
- Checkout: One team owns the funnel, validation rules, payment states, and release cycle.
- Account management: Another team owns profile flows, preferences, and security settings.
- Search and discovery: A separate team owns filtering logic, result rendering, and experimentation.
That kind of split maps architecture to actual operating reality.
What changes with Module Federation
Module Federation lets you break the app apart while still composing it into one runtime experience. The user still sees one application. Teams get separate delivery paths.
That combination matters. It means you can migrate piece by piece instead of pausing product work for a giant frontend rebuild.
Practical rule: If a team can't describe its domain boundary in one sentence, it probably shouldn't be a remote yet.
At this point, teams usually get relief first:
Release independence
A team can ship a feature without waiting for a full app rebuild.Safer migration
You can carve out one domain at a time instead of replacing the whole frontend.Clearer ownership
Bugs become easier to route because responsibility is tied to a bounded area.
Module Federation won't fix weak architecture by itself. If your domains are fuzzy, your contracts are informal, and your shared dependencies are unmanaged, the app will still break. It will just break across repository boundaries.
Understanding the Core Concepts of Module Federation
Before touching configuration, get the mental model right. Module Federation was introduced in Webpack 5 as a runtime mechanism for loading remote modules from separate builds, and webpack documents that each build can act as both a container and a consumer of other builds. Webpack also notes that a host can load code asynchronously at runtime instead of bundling every dependency upfront, and that shared modules usually point to the same library version across builds. In React apps, that's why teams commonly share React and React DOM to avoid shipping duplicate framework copies into the same page, as described in the Webpack Module Federation concepts documentation.

Host, remote, and shared
Think of a shopping mall.
The host is the building itself. It owns the entry, common navigation, shell layout, and the rules for how visitors move through the place. In React terms, that's usually your shell application.
The remote is one store inside the mall. It has its own inventory, staff, release schedule, and internal decisions. In a federated React setup, a remote exposes specific modules the host can load at runtime.
Shared dependencies are the infrastructure everyone uses. You don't want every store installing a separate escalator. In the same way, you don't want multiple React runtimes fighting each other on one page.
Why runtime loading matters
Traditional frontend composition often happens at build time. You import code, bundle everything together, and deploy one artifact. That's simple, but it forces coordination.
Module Federation React setups work differently. The host can load remote code when the application runs. That shifts the integration point from build time to runtime.
That sounds like a small technical distinction, but it changes team workflows:
| Concept | In practice |
|---|---|
| Host | Boots the app, defines routes, loads remotes |
| Remote | Exposes a feature or component from a separate build |
| Shared | Prevents duplicate framework and library instances |
| Runtime composition | Lets teams deploy separately without rebuilding the whole app |
A good host feels boring. It doesn't own business logic it doesn't need. It coordinates.
The part most tutorials skip
The host shouldn't know everything about every remote. It should know how to load a remote and how to interact with it through stable contracts. That means the shell handles composition, not deep internal coupling.
When teams get this wrong, the shell becomes a new monolith wearing a micro-frontend costume. The code sits in different builds, but one app still controls all the decisions. That's not federation. That's distributed tight coupling.
Building Your First Host and Remote React Apps
The fastest way to understand Module Federation React is to build one host and one remote with the smallest possible surface area. Don't start with auth, routing, design systems, and cross-app state all at once. Expose a single component and render it in a shell.
In a React micro-frontend setup, Module Federation is typically implemented with a host shell that loads independently deployed remote applications at runtime. That lets teams develop, test, and ship features separately instead of rebuilding one monolith for every change. A practical pattern is to define bounded business domains, expose only the remote entry points the shell needs, and keep shared libraries isolated so each remote can still own its own dependencies and deployment cycle, as described in this guide to building a micro-front-end architecture with React and Module Federation. If you want a broader implementation view, Nerdify also has a useful article on React micro-frontends.
A minimal project layout
Create two apps:
- host-app
- products-remote
Each app gets its own build, its own package.json, and its own webpack configuration. That separation is the point. If you keep both features in one build system and only simulate separation with folders, you won't learn the constraints that matter.
A simple structure looks like this:
host-app/src/bootstrap.jsxhost-app/src/App.jsxhost-app/webpack.config.jsproducts-remote/src/ProductBadge.jsxproducts-remote/src/bootstrap.jsxproducts-remote/webpack.config.js
Remote configuration
The remote exposes a component. Keep it small.
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
module.exports = {
// usual entry, output, devServer, babel-loader config omitted for brevity
plugins: [
new ModuleFederationPlugin({
name: "productsRemote",
filename: "remoteEntry.js",
exposes: {
"./ProductBadge": "./src/ProductBadge",
},
shared: {
react: { singleton: true },
"react-dom": { singleton: true },
},
}),
],
};
A matching component might be as plain as this:
import React from "react";
export default function ProductBadge() {
return <span>Loaded from remote</span>;
}
Host configuration
The host points to the remote entry and imports the exposed module.
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: "hostApp",
remotes: {
productsRemote: "productsRemote@http://localhost:3001/remoteEntry.js",
},
shared: {
react: { singleton: true },
"react-dom": { singleton: true },
},
}),
],
};
Then consume it with React.lazy:
import React, { Suspense } from "react";
const ProductBadge = React.lazy(() => import("productsRemote/ProductBadge"));
export default function App() {
return (
<div>
<h2>Host shell</h2>
<Suspense fallback={<div>Loading remote...</div>}>
<ProductBadge />
</Suspense>
</div>
);
}
What usually breaks first
Your first errors usually come from one of these:
Wrong remote path
The host can't findremoteEntry.js, so the import fails before React even renders.Unshared React
If React isn't configured properly inshared, hooks start behaving strangely.Overexposing modules
Teams expose half the app instead of a deliberate public API.Missing async bootstrap
Many setups need a bootstrap file pattern so webpack can initialize shared scope cleanly.
Keep the first remote dumb. One exposed component, one host consumer, one clear success condition.
The first successful render matters because it proves the runtime contract works. After that, you can add real feature boundaries, route integration, and production delivery concerns.
Mastering the Webpack ModuleFederationPlugin
Once the first remote renders, the hard part starts. Most production issues don't come from the concept of federation. They come from sloppy plugin configuration.
The ModuleFederationPlugin isn't long, but every property carries architectural weight. A casual config can work in development and still fail badly once multiple teams start deploying independently.

What each key option actually does
Here's a realistic remote configuration:
new ModuleFederationPlugin({
name: "checkout",
filename: "remoteEntry.js",
exposes: {
"./CheckoutApp": "./src/CheckoutApp",
"./routes": "./src/routes",
},
shared: {
react: {
singleton: true,
requiredVersion: false,
},
"react-dom": {
singleton: true,
requiredVersion: false,
},
"react-router-dom": {
singleton: true,
},
},
});
And a matching host example:
new ModuleFederationPlugin({
name: "shell",
remotes: {
checkout: "checkout@http://localhost:3002/remoteEntry.js",
},
shared: {
react: {
singleton: true,
requiredVersion: false,
},
"react-dom": {
singleton: true,
requiredVersion: false,
},
},
});
Now the practical meaning:
nameidentifies the container. Keep it stable and explicit.filenameis the remote entry file that consumers load.exposesdefines the public contract. If it isn't meant for other builds, don't expose it.remotestells the host where to find external containers.sharedcontrols dependency reuse and conflict behavior.
Shared is where architecture gets real
Most Module Federation React bugs trace back to shared. Teams either undershare or overshare.
Undershare React, and you risk duplicate runtimes. Overshare everything, and you tie remotes too tightly to host decisions. Shared libraries should be intentional, not aspirational.
A useful rule of thumb:
| Library type | Usually shared | Usually not shared |
|---|---|---|
| React runtime | Yes | Rarely a good idea not to |
| Routing core | Sometimes | If remotes own isolated routing |
| Design tokens | Sometimes | If versioning is unstable |
| Feature-specific libs | No | Let remotes own them |
Eager loading and exposure discipline
Teams often ask whether to use eager shared modules. My advice is simple. Don't reach for eager loading unless you have a specific reason and you understand the startup cost and loading order consequences.
Most apps do better with default runtime loading because it preserves the decoupling benefit. Eager configuration can be useful in edge cases, but it also makes debugging startup behavior more subtle.
Field note: Expose feature entry points, not internal implementation files. Public API discipline matters more in frontend federation than most teams expect.
A bad exposes block looks like this:
exposes: {
"./Button": "./src/components/Button",
"./useCheckoutState": "./src/hooks/useCheckoutState",
"./helpers": "./src/utils/helpers",
"./constants": "./src/constants",
}
That turns one remote into a dependency dumping ground.
A better approach exposes one feature boundary:
exposes: {
"./CheckoutApp": "./src/CheckoutApp"
}
Then the remote owns its own internals. That keeps refactoring local and contracts stable.
What works in production
The plugin works best when the config mirrors ownership:
- One remote per business domain beats one remote per random component group.
- Small public surfaces beat exposing internals for convenience.
- Conservative shared dependencies beat global sharing as a default.
- Stable remote names beat clever naming schemes that drift between environments.
If your webpack config reads like a dependency treaty between five teams, the architecture already needs attention.
Real-World Patterns and Common Pitfalls
A toy demo proves the mechanism. Production work tests whether the system is governable.
A common pattern in real deployments is to combine Module Federation runtime loading with a shell-managed communication layer, because micro-frontends often need props for local configuration and browser events for cross-cutting actions such as auth, analytics, and notifications. The biggest failure mode is weak contract management. If shared dependency versions drift or event and prop interfaces aren't standardized, teams create integrations that are harder to debug than the monolith they replaced. Guidance from practitioners also emphasizes automated CI/CD, integration testing with mocked APIs, and clear ownership boundaries because manual coordination doesn't scale across independent deployments, as discussed in these Module Federation learnings from production.

Communication patterns that hold up
Not every remote needs to talk to every other remote. That's the first design constraint worth protecting.
What usually works:
Props for local setup
The shell passes environment-specific values, user context fragments, feature flags, or callbacks into a remote.Browser events for cross-cutting concerns
Auth refresh, analytics triggers, and notification events often fit event-based communication better than deep imports.Shell-owned services
The host owns routing decisions, global error boundaries, and top-level session concerns.
What usually goes wrong is direct remote-to-remote dependency. One remote imports another remote's internal state helper, then another team updates it, and suddenly two independently deployed apps are coupled through an unofficial backchannel.
Routing without chaos
Routing is where architecture gets political fast.
One practical approach is to let the shell own top-level routes and let remotes own nested views inside their domain. The shell decides that /account/* belongs to the account remote. The account remote decides what happens under that branch.
That split keeps navigation coherent without forcing the shell to understand every internal screen.
If the shell owns every route detail, remotes aren't autonomous. If remotes invent top-level paths freely, the platform becomes inconsistent.
Contract management is not optional
The discipline federation requires is often underestimated. The code can be correct and the system can still be fragile because the contracts are weak.
A healthy contract includes:
Explicit exposed modules
Not “import whatever you need from our remote.”Version awareness for shared libraries
Especially around React-adjacent tooling.Typed event payloads and prop shapes
If an event exists, document it like an API.Ownership boundaries
Every remote should have a team that owns support, release decisions, and breakage response.
Here's a simple contrast:
| Weak pattern | Stronger pattern |
|---|---|
| Host reaches into remote internals | Host consumes one exposed feature API |
| Ad hoc custom events | Named, documented events with typed payloads |
| Shared everything | Shared only what benefits from reuse |
| Manual release coordination | Pipeline-driven checks before promotion |
The harsh truth is that Module Federation React makes organizational problems visible. If teams don't agree on interfaces, release expectations, and support ownership, the runtime architecture won't save them.
Advanced Strategies for SSR, Testing, and CI/CD
Most tutorials stop at browser-only composition. Real systems don't. If your app needs search visibility, fast first paint, or a content-heavy landing flow, you'll eventually run into SSR and hydration.
That's where federation gets more interesting and less forgiving.
A major underserved area is server-side rendering and hydration with React Module Federation. Many guides still focus on shell-and-remotes in the browser, but production systems often need SSR for SEO, first paint, and performance. Nx documents Module Federation with SSR for React and Angular, including host and remote orchestration and watch-mode workflows, and it also highlights how shared modules and loading behavior become more complex once SSR enters the picture. That challenge is especially relevant in content-heavy products and commerce, as covered in the Nx guide to Module Federation with SSR. For teams evaluating a framework-specific path, this broader discussion of Next.js micro-frontends is a useful companion.

SSR without hydration pain
The hard part isn't exposing a remote. The hard part is making the server-rendered output match what the client hydrates later.
The usual failure points are familiar:
- Different render paths between server and client
- Duplicate data fetching in both shell and remote
- Shared state assumptions that only hold in the browser
A safe pattern is to keep SSR boundaries explicit. Decide which parts of the page must render on the server, which remotes can defer safely, and where data ownership lives. If both shell and remote fetch the same resource independently, you invite mismatches.
SSR in a federated app works best when data ownership is as deliberate as module ownership.
Testing in layers
Testing a federated app as one giant end-to-end black box creates slow feedback and vague failures. A layered approach works better.
- Remote-level tests check the remote in isolation with mocked APIs and stable local fixtures.
- Contract tests verify prop shapes, exposed modules, and event payload expectations.
- Shell integration tests confirm the assembled experience works with real runtime loading.
- End-to-end tests cover the critical journeys only.
That mix catches most failures earlier than full environment testing alone.
CI/CD that matches the architecture
Independent deployment only works if the pipeline enforces confidence. Otherwise teams just move coordination pain from release meetings into chat.
A practical CI/CD model usually includes:
| Pipeline stage | What it should prove |
|---|---|
| Remote build | The remote compiles and exposes the expected modules |
| Remote tests | Local feature behavior still works |
| Contract verification | The host-facing API hasn't drifted unexpectedly |
| Host integration | The shell can still compose the remote correctly |
| Promotion | The artifact is ready to release independently |
The key idea is simple. Build and release boundaries must match runtime boundaries. If every remote deployment still requires a full platform ceremony, you haven't realized the benefits of federation. You've just added moving parts.
Frequently Asked Questions
Is Module Federation only for Webpack
The core Module Federation capability covered in this article is a Webpack 5 feature. That's the native foundation. You may find ecosystem tools and bundler-specific adaptations elsewhere, but if you're talking about the original mechanism and plugin model, you're talking about Webpack.
For teams choosing architecture today, that means one practical question matters more than ideology. Does your build stack support the runtime composition model you need, with the operational behavior you can maintain?
Should multiple remotes share one global state store
Usually, no.
A single cross-app store sounds convenient, but it often creates hidden coupling. One remote starts depending on global assumptions owned by another team, and independent deployment gets weaker. A better pattern is to keep state local to each remote and let the shell own only global concerns such as authenticated user context, layout mode, or feature flags.
If multiple remotes need to react to the same business event, pass props from the shell or publish a documented browser event instead of sharing internal store logic.
How is this different from using iframes
Iframes isolate aggressively. Module Federation composes at the application level.
Use an iframe when hard isolation matters more than shared UX. That can make sense for legacy embeds, strict sandboxing, or systems with very different stacks. Use Module Federation when you want one cohesive React experience with shared dependencies, shared navigation patterns, and runtime composition between independently deployed parts.
When is Module Federation the wrong choice
It's usually the wrong choice when the team is small, the app is still simple, or the organization doesn't have stable domain ownership. Federation adds distributed-system-style complexity to the frontend. If your real problem is messy code inside one product area, splitting the build won't solve that.
It works best when the delivery model already demands separation and the team is ready to treat contracts like product APIs.
If your React app has outgrown monolithic delivery, Module Federation can be the right next step. The win isn't just technical composition. It's giving teams a way to ship independently without making the user experience feel stitched together.