DEV Community

Ishaan Pandey
Ishaan Pandey

Posted on • Originally published at ishaaan.hashnode.dev

Micro Frontends: The Ultimate Guide — React, Vite, Next.js, React Native & Module Federation

Micro Frontends: The Ultimate Guide — React, Vite, Next.js, React Native & Module Federation

"Microservices for the frontend." — said by someone who had not yet tried to share authentication state across three teams' apps in production.

Micro frontends are one of the most hyped, most misunderstood, and most misused architectures in modern frontend development. They're also genuinely the right answer for some teams — and the entirely wrong answer for most others.

This guide is the one I wish I had when I first hit the wall on a 200-person frontend monorepo. We'll go from "what is this even" to building real micro frontends in React + Vite, Next.js, and React Native — with Module Federation, multi-zones, super apps, and every trade-off explained honestly.

No hype, no bias. Just the architecture, the code, and the scars.


Table of Contents

  1. What Are Micro Frontends?
  2. Why Micro Frontends Exist — The Real Problem They Solve
  3. The Five Core Principles
  4. When You Should (and Shouldn't) Use Them
  5. The Composition Strategies
  6. Module Federation — Deep Dive
  7. React + Vite + Module Federation — Full Example
  8. Next.js Micro Frontends — Multi-Zones & Federation
  9. Mobile Micro Frontends — React Native, Expo & Super Apps
  10. Routing Across Micro Frontends
  11. Shared State & Communication
  12. Shared Dependencies — The #1 Pain Point
  13. Styling Isolation
  14. Deployment Strategies
  15. Testing Micro Frontends
  16. Real-World Architectures
  17. The Honest Pitfalls List
  18. The Tools Landscape
  19. Decision Framework
  20. The Future of Micro Frontends

What Are Micro Frontends?

Micro frontends are an architectural style where a frontend application is decomposed into independently buildable, deployable, and ownable pieces — each typically owned by a different team — that are composed together at build time, server side, or run time into a single user-facing experience.

If that sounds like microservices, you're not wrong. The pattern is the spiritual heir to the same idea: take one giant codebase, split it along team lines, and let each team ship on their own schedule.

+---------------------------------------------------------------+
|                       MONOLITH FRONTEND                        |
|  +---------+  +---------+  +---------+  +---------+           |
|  | Header  |  | Search  |  | Product |  | Checkout|           |
|  +---------+  +---------+  +---------+  +---------+           |
|        ALL OWNED BY ONE TEAM, ONE REPO, ONE DEPLOY            |
+---------------------------------------------------------------+

                              vs

+---------------------------------------------------------------+
|                      MICRO FRONTEND SHELL                     |
|  +-----------+  +-----------+  +-----------+  +-----------+   |
|  |  Header   |  |  Search   |  |  Product  |  |  Checkout |   |
|  | (Team A)  |  | (Team B)  |  | (Team C)  |  | (Team D)  |   |
|  | repo A    |  | repo B    |  | repo C    |  | repo D    |   |
|  | deploy A  |  | deploy B  |  | deploy C  |  | deploy D  |   |
|  +-----------+  +-----------+  +-----------+  +-----------+   |
|         ASSEMBLED AT BUILD/SERVER/RUN TIME INTO ONE UI         |
+---------------------------------------------------------------+
Enter fullscreen mode Exit fullscreen mode

The key word in that definition is independently. If "team A has to coordinate a release with team B", you don't have micro frontends — you have a distributed monolith with extra steps.


Why Micro Frontends Exist — The Real Problem They Solve

Let's be brutally honest. Micro frontends do not exist because of a technical problem. They exist because of an organizational one.

The Symptom

You have a frontend codebase. It started small. Now there are 60 engineers committing to it. Pull requests sit in review for 3 days. CI takes 45 minutes. The release train runs once a week and a single broken test blocks 14 features. Everyone is afraid to refactor anything. Standups are 80% coordination overhead.

Sound familiar? That's the moment people start Googling "micro frontends".

The Real Cause

Conway's Law: "Any organization that designs a system will produce a design whose structure is a copy of the organization's communication structure."

If your org chart has 8 product squads but your codebase has 1 frontend app, those 8 squads are going to step on each other forever. The build is shared. The deploy is shared. The runtime is shared. There's no isolation, so there's no autonomy.

Micro frontends are an attempt to make the architecture match the org chart — to give each squad its own frontend the way they already have their own backend service.

What MFE Actually Buys You

  • Independent deploys — Team A ships at 2pm, Team B ships at 2:01pm, neither blocks the other.
  • Independent tech choices — Team A can be on React 19, Team B can still be on React 17 during a migration. (This is also a curse, see later.)
  • Independent CI/CD — No more 45-minute monorepo builds.
  • Smaller blast radius — A bug in Team C's checkout doesn't crash Team A's homepage.
  • Easier onboarding — New hires only need to learn one squad's slice.

What MFE Does Not Buy You

  • It does not make your app faster. (Usually the opposite.)
  • It does not make your code cleaner. (Usually the opposite, at first.)
  • It does not solve bad organizational communication. (It exposes it.)
  • It is not free. The complexity tax is real and permanent.

The Five Core Principles

Cam Jackson's martinfowler.com article laid out what most people now consider the canonical principles. Here they are, with the things people miss:

1. Be Technology Agnostic

Each team should be able to choose its own stack. In practice, don't. Pick one framework as the default and only deviate when there's a compelling reason. Mixing React, Vue, and Svelte in one app is a recipe for a 5MB shared bundle and a debugging nightmare.

2. Isolate Team Code

No shared runtime state by default. No global CSS leaking between teams. Build for the assumption that other teams' code is a black box.

3. Establish Team Prefixes

Use naming conventions for CSS, events, local storage, cookies, etc. — team-checkout-cart-open, team-search-results — so collisions are impossible.

4. Favor Native Browser Features

Custom events, the URL, postMessage, broadcast channels — these all exist for a reason and they cost zero coordination overhead. Reach for these before a custom pub/sub bus.

5. Build a Resilient Site

A single broken micro frontend should never take down the whole page. Use error boundaries. Use timeouts. Use fallbacks. Treat every remote like an unreliable network call, because that's exactly what it is.


When You Should (and Shouldn't) Use Them

This is the section nobody writes honestly. Here's the honest version.

You Probably Should NOT Use Micro Frontends If…

  • You have fewer than 3 product teams sharing the frontend. You'll spend more on architecture than you save in autonomy.
  • Your app is performance-critical for first-load (landing pages, marketing sites, e-commerce funnels). The extra HTTP requests and larger combined bundles will hurt your Core Web Vitals.
  • You have heavy cross-team UX flows — checkout flows, wizards, dashboards where every section talks to every other section. Forcing these across MFE boundaries is masochism.
  • You're building a mobile-first product. Mobile MFE is real but it's hard mode (more on that later).
  • You think MFE will fix bad code. It will not. It will give bad code more places to hide.

You Probably SHOULD Use Micro Frontends If…

  • You have 5+ product teams all blocked on a shared frontend.
  • Your product is naturally page- or section-based with weak coupling between sections (think: an enterprise dashboard with 12 mostly independent modules, or a marketplace with seller tools, buyer tools, admin tools).
  • You're doing a legacy migration — you can use MFE to incrementally replace pieces of an old app without a big-bang rewrite. This is genuinely the killer use case.
  • You're building a platform / super app where third parties (or other internal teams) ship modules into your shell.

The Honest Heuristic

If you're asking "should we do micro frontends?", the answer is almost always no.

The teams that should do them are the teams whose pain has already gotten so bad that they didn't have to ask.


The Composition Strategies

Once you've decided to do MFE, the next question is: how do the pieces actually get glued together? There are five real strategies. Each one trades off something different.

+---------------------------------------------------------------+
|              MICRO FRONTEND COMPOSITION STRATEGIES             |
+---------------------------------------------------------------+
|                                                                |
|   1. BUILD-TIME (npm packages)                                 |
|      Each team publishes a package, host installs them.       |
|      [+] Simple, type-safe                                    |
|      [-] Requires host redeploy on every change. NOT REAL MFE.|
|                                                                |
|   2. SERVER-SIDE COMPOSITION (SSI / ESI / Tailor / Podium)    |
|      A server stitches HTML fragments from multiple services. |
|      [+] Great for SEO, fast first paint                      |
|      [-] Server complexity, harder client-side interactivity  |
|                                                                |
|   3. IFRAMES                                                  |
|      The OG. Each MFE in its own iframe.                      |
|      [+] Bulletproof isolation                                |
|      [-] Routing, sizing, deep linking, SSO are all painful   |
|                                                                |
|   4. WEB COMPONENTS                                           |
|      Each MFE ships as a custom element. The shell uses tags. |
|      [+] Standards-based, framework-agnostic                  |
|      [-] Shadow DOM friction with React, styling pain         |
|                                                                |
|   5. RUNTIME JS (Module Federation / single-spa / SystemJS)   |
|      The shell loads remote JS bundles at runtime.            |
|      [+] True independence + shared dependencies              |
|      [-] Most complex, biggest perf footgun                   |
|                                                                |
+---------------------------------------------------------------+
Enter fullscreen mode Exit fullscreen mode

Let's go quickly through each, then go deep on the one you'll actually use: Module Federation.

1. Build-Time Integration (NPM Packages)

Each team publishes their feature as an npm package. The host app npm installs them and ships them as part of its bundle.

// host package.json
{
  "dependencies": {
    "@acme/header": "^1.2.0",
    "@acme/checkout": "^3.4.0",
    "@acme/product": "^2.1.0"
  }
}
Enter fullscreen mode Exit fullscreen mode

The catch: to ship a checkout fix, the checkout team has to publish a new version, the host team has to bump it, and the host has to redeploy. That's not independent — that's a coordinated release with extra steps. This is not real micro frontends, but it can be a fine starting point.

2. Server-Side Composition

A server fetches HTML fragments from multiple backend services and stitches them together before sending the response to the browser. Tools like Tailor (Zalando), Podium (Finn.no), Edge Side Includes (Akamai/Varnish), and Next.js's own streaming SSR all live in this family.

<!-- The composed page -->
<html>
  <body>
    <!--# include virtual="/header" -->
    <!--# include virtual="/product" -->
    <!--# include virtual="/footer" -->
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

Strengths: great SEO, fast first paint, works with any backend stack.
Weaknesses: complex infra, client-side interactivity has to be re-hydrated per fragment, debugging is harder.

3. Iframes

Yes, iframes. Don't laugh — iframes are the only composition strategy with truly bulletproof isolation. No CSS leaking, no JS conflicts, no shared globals. Spotify's desktop app famously used iframes for years.

The pain is everything around the iframe: routing, deep links, sizing, scroll restoration, single sign-on, sharing user state, focus management, accessibility. You can solve every one of these but each one costs you a week.

Use iframes when isolation is non-negotiable — for example, when you're embedding third-party modules you don't trust.

4. Web Components

Each MFE compiles to a custom element. The shell uses it like any other HTML tag.

<header-mfe></header-mfe>
<product-mfe sku="xyz"></product-mfe>
<checkout-mfe></checkout-mfe>
Enter fullscreen mode Exit fullscreen mode

This is the most "framework agnostic" approach. It works. It's standards-based. But shadow DOM has friction with most frameworks (especially anything that wants to portal modals), styling becomes a saga, and you give up the ergonomics of your favorite framework's component model at the boundary.

5. Runtime JS Composition (The One You'll Probably Use)

The shell loads remote JavaScript bundles at run time. The two big approaches are:

  • single-spa — an opinionated framework for orchestrating multiple SPAs in one shell.
  • Webpack Module Federation (and its successors in Vite, Rspack, Next.js, Re.Pack) — a more granular approach where you can share individual modules, not just whole apps.

Module Federation has eaten the world. Let's go deep.


Module Federation — Deep Dive

Module Federation was introduced in Webpack 5 by Zack Jackson in 2020. The core idea is dead simple but the implications are huge:

Any build can dynamically load code from any other build at run time, with shared dependencies properly deduplicated.

The Concepts

+-----------------+        +-----------------+
|     HOST        |        |     REMOTE      |
|  (the shell)    |<-------|  (a feature)    |
|                 |        |                 |
| Loads remotes   |        | Exposes modules |
| Defines shared  |        | Declares shared |
+-----------------+        +-----------------+
        ^                          ^
        |                          |
        +-----------+   +----------+
                    |   |
                    v   v
            +---------------------+
            |      SHARED         |
            |  (react, react-dom, |
            |   react-router...)  |
            +---------------------+
            Loaded once, used by both
Enter fullscreen mode Exit fullscreen mode

There are three pieces of vocabulary you need:

  • Host: an app that consumes modules from other apps.
  • Remote: an app that exposes modules for other apps to consume.
  • Shared: dependencies (like React) that should be loaded once and shared across host and remotes — not bundled twice.

A single app can be both a host and a remote at the same time. That's where the power comes from — it's a peer-to-peer network of bundles, not a strict hub-and-spoke.

What Gets Generated

When you build a remote, Module Federation produces a remoteEntry.js file. That file is the manifest of what the remote exposes — it's tiny, and the actual code chunks are loaded lazily on demand.

remote-app/dist/
├── remoteEntry.js          <- The manifest. Loaded by the host first.
├── src_Button_tsx.js       <- The actual exposed module.
├── src_Header_tsx.js
└── vendor.js
Enter fullscreen mode Exit fullscreen mode

The host points at https://remote.example.com/remoteEntry.js, fetches the manifest, then asks for individual exposed modules as the user navigates.

The Critical Concept: shared

This is where 90% of all Module Federation bugs live.

When you mark react as shared, Module Federation runs a tiny negotiation at run time:

  1. Host has react@18.2.0.
  2. Remote also has react@18.2.0.
  3. Both agree to use the host's copy. One React, problem solved.

But:

  1. Host has react@18.2.0.
  2. Remote has react@17.0.2.
  3. Without a constraint, you now have two Reacts on the same page, which means hooks break, contexts don't cross boundaries, and your dev tools lie to you.

The fix is singleton: true and requiredVersion:

shared: {
  react: {
    singleton: true,        // There can be only one
    requiredVersion: '^18.0.0',
    strictVersion: true,    // Throw, don't silently dual-load
  },
  'react-dom': { singleton: true, requiredVersion: '^18.0.0', strictVersion: true },
}
Enter fullscreen mode Exit fullscreen mode

Memorize this. Anything stateful (React, Redux, Zustand, react-router, react-query) MUST be a singleton across federated boundaries, or you will lose hours to ghost bugs.

Eager vs Lazy

By default, federated modules load lazily — that's the whole point. But the host's index.tsx runs before the federation runtime is ready, so you usually need a "bootstrap" pattern:

// index.tsx
import('./bootstrap');
Enter fullscreen mode Exit fullscreen mode
// bootstrap.tsx
import React from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
createRoot(document.getElementById('root')).render(<App />);
Enter fullscreen mode Exit fullscreen mode

That dynamic import() gives Module Federation a chance to initialize the shared scope before any of your code touches React. Skip this and you'll get cryptic "Shared module is not available for eager consumption" errors.


React + Vite + Module Federation — Full Example

Time for code. Let's build the smallest possible end-to-end MFE: a Vite + React shell that loads a remote Vite + React Button at run time.

We'll use @module-federation/vite (the official plugin from the Module Federation team — there's also @originjs/vite-plugin-federation which is the older community option, but the official one is now the recommended path).

Project Structure

mfe-demo/
├── shell/         (host)
│   ├── src/
│   │   ├── main.tsx
│   │   ├── bootstrap.tsx
│   │   └── App.tsx
│   ├── vite.config.ts
│   └── package.json
└── remote-button/ (remote)
    ├── src/
    │   ├── main.tsx
    │   ├── bootstrap.tsx
    │   └── Button.tsx
    ├── vite.config.ts
    └── package.json
Enter fullscreen mode Exit fullscreen mode

The Remote — remote-button/vite.config.ts

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { federation } from '@module-federation/vite';

export default defineConfig({
  plugins: [
    react(),
    federation({
      name: 'remoteButton',
      filename: 'remoteEntry.js',
      exposes: {
        './Button': './src/Button.tsx',
      },
      shared: {
        react: { singleton: true, requiredVersion: '^18.0.0' },
        'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
      },
    }),
  ],
  build: {
    target: 'esnext',
    modulePreload: false,
    cssCodeSplit: false,
  },
  server: { port: 5174 },
  preview: { port: 5174 },
});
Enter fullscreen mode Exit fullscreen mode

The Remote — remote-button/src/Button.tsx

import { useState } from 'react';

export default function Button({ label }: { label: string }) {
  const [count, setCount] = useState(0);
  return (
    <button
      onClick={() => setCount((c) => c + 1)}
      style={{
        padding: '10px 20px',
        background: '#6366f1',
        color: 'white',
        border: 'none',
        borderRadius: 8,
      }}
    >
      {label} — clicked {count} times
    </button>
  );
}
Enter fullscreen mode Exit fullscreen mode

The Host — shell/vite.config.ts

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { federation } from '@module-federation/vite';

export default defineConfig({
  plugins: [
    react(),
    federation({
      name: 'shell',
      remotes: {
        remoteButton: 'http://localhost:5174/assets/remoteEntry.js',
      },
      shared: {
        react: { singleton: true, requiredVersion: '^18.0.0' },
        'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
      },
    }),
  ],
  build: { target: 'esnext' },
  server: { port: 5173 },
});
Enter fullscreen mode Exit fullscreen mode

The Host — shell/src/main.tsx

// Bootstrap pattern — required for Module Federation
import('./bootstrap');
Enter fullscreen mode Exit fullscreen mode

The Host — shell/src/bootstrap.tsx

import React from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
createRoot(document.getElementById('root')!).render(<App />);
Enter fullscreen mode Exit fullscreen mode

The Host — shell/src/App.tsx

import { lazy, Suspense } from 'react';

// TypeScript needs to be told this module exists
// (we'll fix that with a .d.ts in a sec)
const RemoteButton = lazy(() => import('remoteButton/Button'));

export default function App() {
  return (
    <div style={{ padding: 40 }}>
      <h1>Shell App</h1>
      <p>The button below is loaded from a different deployment at runtime:</p>
      <Suspense fallback={<div>Loading button…</div>}>
        <RemoteButton label="Click me" />
      </Suspense>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

TypeScript: Telling It About the Remote

Federated imports are dynamic strings — TypeScript has no idea they exist. Add a type declaration:

// shell/src/types/remotes.d.ts
declare module 'remoteButton/Button' {
  const Button: React.ComponentType<{ label: string }>;
  export default Button;
}
Enter fullscreen mode Exit fullscreen mode

Running It

# Terminal 1
cd remote-button && npm run build && npm run preview

# Terminal 2
cd shell && npm run dev
Enter fullscreen mode Exit fullscreen mode

Open http://localhost:5173 and you'll see the shell app with the button rendered from a completely separate build on :5174. Update the button's code, rebuild only the remote, refresh the shell — and the new button is live without rebuilding the host.

That's the magic moment. Independent deploys, true and live.

Common Vite + MF Gotchas

  • Dev mode: Module Federation in Vite works best when remotes are built and previewed (vite build && vite preview), not just run via vite dev. Dev-mode HMR for remotes is improving but still flaky.
  • CORS: the host has to be able to fetch remoteEntry.js. Remotes need permissive CORS in production.
  • Asset paths: make sure the remote's base is set correctly when deployed to a subpath/CDN, otherwise its lazy chunks 404.
  • CSS: federated CSS is the wild west. The safest path is to scope all styles via CSS Modules or styled-components and let each remote ship its own.

Next.js Micro Frontends — Multi-Zones & Federation

Next.js MFE is a different beast because Next isn't just a bundler — it's a full SSR framework with its own router, its own data fetching, and (in App Router) its own server component model. There are two real approaches.

Approach 1: Multi-Zones (the simple way)

Multi-zones is built into Next.js itself. The idea: you run multiple separate Next.js apps, each handling a different URL prefix, and a top-level reverse proxy (or rewrites config) stitches them together so the user sees one domain.

// next.config.js (the "main" zone)
module.exports = {
  async rewrites() {
    return [
      { source: '/blog', destination: 'https://blog-zone.vercel.app/blog' },
      { source: '/blog/:path*', destination: 'https://blog-zone.vercel.app/blog/:path*' },
      { source: '/store', destination: 'https://store-zone.vercel.app/store' },
      { source: '/store/:path*', destination: 'https://store-zone.vercel.app/store/:path*' },
    ];
  },
};
Enter fullscreen mode Exit fullscreen mode

Each zone is a normal Next.js app — its own repo, its own deploy, its own data layer. The user's browser hits example.com/blog and the main zone's edge transparently proxies to the blog zone.

When this is enough:

  • Your app is naturally split by URL section.
  • You don't need to share runtime React state between zones.
  • You're okay with a full page navigation when crossing zones (Next 13+ does soft-nav within a zone, hard-nav across zones).

For 80% of teams asking about Next.js MFE, this is what they actually want. Multi-zones is the boring, working answer. Use it first.

Approach 2: Module Federation in Next.js

When multi-zones isn't enough — you need to share components across teams, not just whole pages — you reach for @module-federation/nextjs-mf (Zack Jackson's plugin).

// next.config.js (host)
const NextFederationPlugin = require('@module-federation/nextjs-mf');

module.exports = {
  webpack(config, options) {
    config.plugins.push(
      new NextFederationPlugin({
        name: 'shell',
        remotes: {
          checkout: `checkout@${process.env.CHECKOUT_URL}/_next/static/${
            options.isServer ? 'ssr' : 'chunks'
          }/remoteEntry.js`,
        },
        shared: {
          react: { singleton: true, requiredVersion: false },
        },
        extraOptions: {
          exposePages: false,
          enableImageLoaderFix: true,
          enableUrlLoaderFix: true,
        },
      })
    );
    return config;
  },
};
Enter fullscreen mode Exit fullscreen mode
// next.config.js (remote)
const NextFederationPlugin = require('@module-federation/nextjs-mf');

module.exports = {
  webpack(config, options) {
    config.plugins.push(
      new NextFederationPlugin({
        name: 'checkout',
        filename: 'static/chunks/remoteEntry.js',
        exposes: {
          './CartWidget': './components/CartWidget.tsx',
          './CheckoutForm': './components/CheckoutForm.tsx',
        },
        shared: {},
      })
    );
    return config;
  },
};
Enter fullscreen mode Exit fullscreen mode

The Catch with Next.js + MF

Next.js does SSR, which means federated components have to be loadable on the server and the client, with the same code path producing the same HTML on both. That's significantly harder than browser-only MFE because:

  • The server has no window, so the remote's remoteEntry.js has to be loaded via Node-side fetch.
  • Hydration mismatches across federated boundaries are the most painful debugging experience in web dev.
  • Streaming SSR + federated components is still rough.
  • App Router (Next 13+) has limited federation support today. As of writing, @module-federation/nextjs-mf has the most reliable support in the Pages Router. App Router support is shipping but more fragile — check the docs before you bet on it.

Pragmatic recommendation for 2026: if you need MFE in Next.js, default to multi-zones. If you absolutely need federated components, run them as client components only ('use client' at the boundary), and accept that you're trading some SSR benefits for the federation.


Mobile Micro Frontends — React Native, Expo & Super Apps

Mobile MFE is a different planet. On the web, you can fetch a JS bundle from a CDN at run time and execute it. On mobile, the OS would very much prefer you not do that — Apple's App Store rules around dynamic code loading are particularly strict.

But it can be done, and there are real reasons to do it: super apps. WeChat, Gojek, Careem, Grab, Alipay, and Rappi all run dozens of independent feature teams shipping into one shell app. Each team has its own release cycle, its own QA, its own analytics — but it all looks like one app to the user.

React Native + Re.Pack + Module Federation

The current best-in-class approach is Re.Pack by Callstack — a Webpack-based bundler for React Native (an alternative to Metro) that supports Module Federation natively.

// rspack.config.mjs (host)
import { ModuleFederationPluginV2 } from '@module-federation/enhanced/rspack';

export default {
  plugins: [
    new ModuleFederationPluginV2({
      name: 'host',
      remotes: {
        miniApp: 'miniApp@http://localhost:9000/miniApp.container.bundle',
      },
      shared: {
        react: { singleton: true, eager: true },
        'react-native': { singleton: true, eager: true },
      },
    }),
  ],
};
Enter fullscreen mode Exit fullscreen mode
// MiniApp.js (remote)
import { Text, View } from 'react-native';
export default function MiniApp() {
  return (
    <View>
      <Text>I'm shipped independently from the host</Text>
    </View>
  );
}
Enter fullscreen mode Exit fullscreen mode

The host fetches the remote bundle at run time, evaluates it inside the JS engine (Hermes/JSC), and renders it. From the user's perspective, it's one app. From the team's perspective, the mini app team can ship updates without going through the App Store review for the shell — only the shell update needs review, not the mini app update.

Expo and OTA Updates

Expo doesn't natively do Module Federation, but it provides something adjacent: EAS Update, an OTA update channel. You can ship JS-only updates over the air after App Store review, which gives you a coarse form of independent deployment.

eas update --branch production --message "Fix checkout bug"
Enter fullscreen mode Exit fullscreen mode

This is not micro frontends — it's a single-team OTA channel. But for many "we just need faster ship cycles" cases, it's the right answer instead of true MFE on mobile.

Why Mobile MFE Is Hard Mode

  • Apple's rules. App Store guideline 3.3.2 historically forbade executing remotely-downloaded code that "changes features or functionality". The interpretation has loosened (React Native and CodePush have been allowed for years), but you're still walking a line. Read the latest guidelines and don't ship something that looks like an app store of its own.
  • No CDN-fast loads. Mobile networks are unreliable. You have to cache aggressively, support offline, and fall back gracefully.
  • Native modules. If two mini apps need different versions of a native module, you're toast. Native dependencies have to be unified at the host level.
  • Bundle size. Every shared dependency duplication is a megabyte of extra app size.
  • Debugging. You have not lived until you've debugged a federated React Native app on a low-end Android device with no source maps.

When Mobile MFE Is Worth It

You are building a super app with 5+ independent product teams shipping into one binary, you have the engineering org to support a custom mobile build infra, and your business model genuinely depends on the modularity. Otherwise, just use Expo + EAS Update and call it a day.


Routing Across Micro Frontends

Routing is the area where MFE gets interesting fast. Three patterns dominate.

1. Single Shell Router

The shell owns the router. Each MFE is a leaf component the shell mounts at a route.

// shell
<Router>
  <Route path="/products/*" element={<ProductMFE />} />
  <Route path="/checkout/*" element={<CheckoutMFE />} />
  <Route path="/account/*" element={<AccountMFE />} />
</Router>
Enter fullscreen mode Exit fullscreen mode

Pros: simple, predictable, the URL is the source of truth.
Cons: the shell has to know about every MFE's top-level routes. Adding a new MFE requires a shell change.

2. Distributed Routing

Each MFE has its own router. The shell delegates to whichever MFE matches the prefix.

// shell — delegates anything under /checkout/* to the checkout MFE
<Route path="/checkout/*" element={<CheckoutMFE />} />

// inside CheckoutMFE
<Routes>
  <Route path="/" element={<Cart />} />
  <Route path="/payment" element={<Payment />} />
  <Route path="/confirm" element={<Confirm />} />
</Routes>
Enter fullscreen mode Exit fullscreen mode

Pros: new sub-routes don't require a shell change.
Cons: each router has to be configured to use the same router type and base path; deep linking and 404s get tricky.

3. single-spa Style "Activity Functions"

single-spa registers each app with an activity function — a predicate that says "this app should be active when…".

registerApplication({
  name: '@team/checkout',
  app: () => import('@team/checkout'),
  activeWhen: (location) => location.pathname.startsWith('/checkout'),
});
Enter fullscreen mode Exit fullscreen mode

The shell never knows about routes per se — it just runs whichever apps match the current location. This is the most decoupled approach but it has the steepest learning curve.

The Underrated Answer: Use the URL as the Integration Layer

Instead of trying to share router state across MFEs, push everything into the URL. Filters, modals, selected items — all in query params. Now any MFE can read its state from the URL without needing a shared store, and deep linking just works.

This single principle eliminates 60% of the "shared state" problems people have with MFE.


Shared State & Communication

The single most painful question in MFE: how do micro frontends talk to each other?

The wrong answer is "a shared Redux store". You just turned your MFEs into a distributed monolith again — every team needs to know the shape of the store, every state change risks breaking everyone, and you've coupled the deploys together.

The right answer is the loosest possible coupling that solves your actual problem. In order from best to worst:

1. The URL (best)

If the state can live in the URL, put it in the URL. Every MFE can read it independently with useSearchParams.

2. Custom Events

Native browser events. Zero dependencies. Loosely coupled.

// Cart MFE publishes:
window.dispatchEvent(new CustomEvent('cart:item-added', { detail: { sku } }));

// Header MFE listens:
useEffect(() => {
  const handler = (e) => updateBadge(e.detail);
  window.addEventListener('cart:item-added', handler);
  return () => window.removeEventListener('cart:item-added', handler);
}, []);
Enter fullscreen mode Exit fullscreen mode

Use a team prefix (cart:, auth:) to avoid collisions.

3. BroadcastChannel

Like custom events, but works across tabs too. Great for "logout in one tab logs out everywhere" patterns.

const channel = new BroadcastChannel('auth');
channel.postMessage({ type: 'logout' });
Enter fullscreen mode Exit fullscreen mode

4. A Tiny Pub/Sub Bus

If custom events feel too anonymous, ship a tiny shared event bus as a federated module. Keep its API frozen — it's a contract.

5. Shared Store (last resort)

If you absolutely need a shared reactive store (Zustand, Redux), expose it as a singleton from the shell. Lock down its public API like you would a public REST API. Treat schema changes like breaking API changes.

The Anti-Pattern: Direct Imports From Other MFEs

The moment one MFE imports a file from another MFE's source, you've recoupled the deploys. Don't do it. Communicate via the boundary.


Shared Dependencies — The #1 Pain Point

I'm going to say this loudly because it's the #1 thing that breaks MFE projects:

Two copies of React on the same page will silently destroy your application.

Hooks throw "invalid hook call". Contexts don't propagate across the boundary. Portals end up in the wrong tree. The dev tools lie about which fiber is which. You will lose days.

The Rules

  1. Anything stateful must be a singleton. React, react-dom, react-router, react-query, redux, zustand, jotai, recoil, styled-components, emotion, framer-motion, react-i18next — all of these break if there are two copies.
  2. Use singleton: true and strictVersion: true on every singleton in your federation config. Yes, that means cross-team upgrades have to be coordinated. That's the price.
  3. Pin the version range carefully. ^18.0.0 says "any 18.x is fine". ~18.2.0 is stricter. The tighter you are, the safer — and the more coupled your teams become. Pick where on this spectrum your org wants to live.
  4. Audit your shared graph. Periodically run a build report to see exactly what's being shared and what isn't. Drift is silent and lethal.

The Honest Trade-off

The dirty secret: "tech agnostic" micro frontends are mostly a lie. Once you mark React as singleton, every team has to be on a compatible React version. Once you mark react-router as singleton, every team has to be on a compatible router version. The freedom to "just use whatever framework you want" evaporates the moment you need shared state.

The realistic version is: all teams agree on a small core (React, react-dom, router, maybe a state lib) and can vary independently outside that core. That's good enough, and far less painful than the dream.


Styling Isolation

CSS leakage between MFEs is the second-most-common bug class after duplicated React.

Strategies, Ranked

  1. CSS Modules — locally scoped class names by default. Boring, works, recommended.
  2. CSS-in-JS with scoped class generation (styled-components, emotion, vanilla-extract) — works, but watch for SSR + federation hydration issues.
  3. Tailwind with team prefixes — viable, but requires every team to use the same Tailwind config or you'll have conflicting utility classes.
  4. Shadow DOM — bulletproof isolation but breaks portals, modals, design systems, and most React libraries you care about.
  5. Plain CSS with team-prefixed class names — works if your teams have discipline. They don't.

The Things You'll Learn the Hard Way

  • !important always wins, and someone will use it.
  • A reset stylesheet loaded twice is its own special hell.
  • Fonts loaded in two MFEs with slightly different weights/subsets will FOUT for both of them.
  • Design tokens (colors, spacing) should live in one shared source of truth, not in each MFE's CSS.

Deployment Strategies

The whole point of MFE is independent deploys. Here's how to actually do them.

Each MFE Is Its Own Deploy Unit

  • Its own repo (or its own folder + CI pipeline in a monorepo).
  • Its own build, its own CI, its own preview environments.
  • Its own CDN bucket / hosting.
  • Its own versioning.

The host doesn't redeploy when a remote ships. That's the whole game.

Versioning: Latest vs Pinned

Two philosophies:

  • "Latest" model: the host always points at https://remote.example.com/latest/remoteEntry.js. New remote ships, host picks it up immediately. Fast iteration, scary blast radius.
  • "Pinned" model: the host points at https://remote.example.com/v1.4.2/remoteEntry.js. Host has to bump the version to roll forward. Safer, slower, partially defeats the point.

The middle ground: use the latest model with a manifest service that lets you roll back instantly by flipping the "current" pointer. CloudFront, S3, or a tiny "versions API" can all serve this.

Canary Releases

Roll out a new remote to 5% of users by serving a different remoteEntry.js URL based on a feature flag or user cohort. Most CDNs and edge platforms (Cloudflare Workers, Vercel Edge Functions) make this trivial.

Cache Busting

remoteEntry.js should be served with Cache-Control: no-cache, must-revalidate. The chunks it references should be content-hashed and served with Cache-Control: public, max-age=31536000, immutable. Two simple rules; getting them wrong is what causes "the new version isn't loading" tickets.


Testing Micro Frontends

Unit Tests

Each MFE tests its own components in isolation. Same as any normal app. The federation boundary doesn't affect this layer.

Contract Tests

This is the layer that matters. You need to verify that the contract between host and remote stays stable across deploys. Two flavors:

  1. Type contracts — generate .d.ts files for every exposed module and publish them as a package. The host imports the types and TypeScript catches breaking changes at build time.
  2. Runtime contracts — write tests in the host that import the real remote and assert it has the expected exports and prop shapes. Run these in CI on every host and remote build.

@module-federation/dts-plugin is the current best tool for auto-generating type contracts.

Integration / E2E

Run the full composed app (shell + all remotes) in a staging environment and run Playwright/Cypress against it. This is where you catch the bugs that only happen across boundaries — hydration mismatches, shared state collisions, duplicate React.

The Rule

Anything that crosses a federation boundary must have a contract test.

Skip this and you ship breakage to other teams every Tuesday.


Real-World Architectures

Let's look at how serious teams actually ship MFE.

Spotify

Famously used iframes for years (the "Spotile" pattern) because the team-isolation guarantee was worth the pain. They've since moved much of the desktop client to a more integrated approach but the iframe-based platform proved that even hated technology can scale if it solves the right problem.

IKEA

Uses a server-side composition approach for product pages — different teams own the gallery, the description, the reviews, and the buy box, and each ships independently. The page is stitched together at the edge.

Zalando

Built and open-sourced Tailor and Mosaic9 — server-side composition tooling that lets dozens of squads ship their own templates into a unified product page. Their SaaS-scale catalog made this a hard requirement.

DAZN

Talked publicly about going from a single-team frontend to a 200+ engineer MFE setup. They use a shell + Module Federation pattern to give each squad full ownership of its slice while keeping the global navigation, auth, and player consistent.

Microsoft (Office on the Web)

Office Online is famously a federation of independently-shipped apps (Word, Excel, PowerPoint) sharing a common shell, identity layer, and design system.

The Common Thread

None of these companies started with micro frontends. They started with monoliths, hit the team-scaling wall, and then refactored to MFE because the org pain was worse than the architectural pain.


The Honest Pitfalls List

I've watched teams adopt MFE and regret it. Here are the failure modes, ranked by how often I see them.

1. Duplicated React (covered above, but it's #1 for a reason)

You will get hit by this within the first month. Read the singleton section twice.

2. The "Distributed Monolith"

Teams nominally have separate MFEs but every change requires coordinated deploys across 4 of them. You have all the complexity of MFE with none of the autonomy.

3. Bundle Size Explosion

Each MFE ships its own copy of utility libs (lodash, date-fns, axios, the design system). Total page weight doubles or triples. Lighthouse cries.

4. Performance Death by 1000 Chunks

The host loads remoteEntry.js, which loads chunks, which load more chunks, which load more chunks. By the time the page renders, you've made 30 network requests. Your TTI tanks.

5. Type Safety at the Boundary

Federated imports are dynamic strings. Without .d.ts generation and contract tests, refactors silently break consumers.

6. Debugging Across Boundaries

Source maps for federated chunks are improving but still painful. Stack traces span multiple builds with different commit hashes. Sentry can handle this; your weekend debugging session might not.

7. The "Shared Design System" Wars

Every team wants to evolve the design system at their own pace. The design system team becomes a bottleneck. You either ship multiple versions (more bundle bloat) or coordinate every change (more coupling).

8. Org Overhead

Each MFE needs CI, CD, on-call rotation, monitoring, deploy pipelines, preview environments. You just multiplied your platform team's workload by N.

9. Onboarding Confusion

"Where does this code live?" becomes a 10-minute conversation instead of a 10-second one.

10. The "Why Are We Doing This Again" Quarterly Review

You will have this meeting. Make sure the answer is still good.


The Tools Landscape

Your options as of 2026:

Tool Best For Notes
Webpack Module Federation The OG. Mature, battle-tested. The reference implementation. Everything else copies it.
Module Federation 2.0 (@module-federation/enhanced) The current state of the art. Adds runtime API, manifest, type generation. Use this on new projects.
@module-federation/vite Vite-based projects. Official Vite plugin. The recommended Vite path.
@originjs/vite-plugin-federation Vite-based projects (community). Older, still works, less actively maintained.
@module-federation/nextjs-mf Next.js with federation needs. Pages Router solid, App Router still maturing.
Next.js Multi-Zones URL-segmented Next.js apps. Built-in. The boring right answer for most Next teams.
Re.Pack React Native. Webpack-based RN bundler with first-class MF support.
single-spa Multi-framework or whole-app composition. Older but still excellent for orchestrating whole SPAs.
Piral Plugin-style portal apps. .NET-friendly, popular in enterprise.
qiankun China-heavy stacks; Vue + React mixing. Built on single-spa, very popular in Asia.
Bit Component-as-a-package approach. Build-time MFE done well.
Native Federation (Angular) Angular shops. Standards-based (ESM + import maps), no Webpack required.
Rspack Webpack-compatible, Rust-based. Drop-in faster Webpack with first-class MF support.
Turbopack Next.js future. MF support is still landing — not yet a primary choice.

The Pragmatic Defaults for 2026

  • React + Vite host: @module-federation/vite + Module Federation 2.0
  • Next.js with section split: Next.js Multi-Zones
  • Next.js with shared components across teams: @module-federation/nextjs-mf (Pages Router) — and watch the App Router space closely
  • React Native super app: Re.Pack + Module Federation 2.0
  • Multi-framework legacy migration: single-spa
  • Component-level reuse without runtime federation: Bit

Decision Framework

Five questions. Answer them honestly.

1. How many teams own the frontend?

  • 1–2 teams → No MFE. Use a monorepo.
  • 3–4 teams → Consider it, but a clean monorepo is still probably better.
  • 5+ teams → MFE starts paying for itself.

2. How coupled are your features?

  • Tightly coupled (every screen uses every other screen's state) → No MFE. It will hurt.
  • Loosely coupled (clear sectional boundaries) → MFE is a fit.

3. How performance-critical is the first paint?

  • Marketing site / e-commerce / SEO-driven → Be very careful. SSR + MFE is hard. Multi-zones may be enough.
  • Internal tool / dashboard / authenticated app → Performance is less critical, MFE is more viable.

4. Do you have platform engineering capacity?

  • No dedicated platform/infra team → Don't. MFE has a permanent operational tax.
  • Yes → Proceed.

5. What's the actual problem you're solving?

  • "Our deploys are slow" → Fix the build first, MFE second.
  • "Our teams can't ship without blocking each other" → MFE is correct.
  • "We want to use different frameworks per team" → Reconsider your life choices, then MFE.

The Decision Tree

                    Do you have 5+ frontend teams?
                              |
                +-------------+-------------+
                |                           |
                NO                          YES
                |                           |
        Monorepo + clean              Are features loosely
        boundaries. Done.             coupled by section?
                                            |
                              +-------------+-------------+
                              |                           |
                              NO                          YES
                              |                           |
                    Refactor for                  Is SSR + SEO
                    isolation first.              critical?
                                                        |
                                          +-------------+-------------+
                                          |                           |
                                          YES                         NO
                                          |                           |
                                    Next.js                    Module Federation
                                    Multi-Zones                (Vite or Webpack)
Enter fullscreen mode Exit fullscreen mode

The Future of Micro Frontends

A few things worth watching as 2026 unfolds:

1. Module Federation 2.0

The current cutting edge. Adds a runtime API (load remotes by URL at run time), manifest format, automatic type generation, and a much better dev experience. Use it on new projects.

2. React Server Components Across Boundaries

RSC is fundamentally hostile to runtime federation today — server components serialize to a wire format that the host has to know about. The future likely involves RSC-aware federation or a complete rethink of how server-rendered fragments compose. This space is moving fast.

3. Edge Composition

Cloudflare, Vercel, Netlify, and Fastly are all making it cheap to compose HTML at the edge. Server-side composition may quietly come back into fashion, with edge functions doing the stitching.

4. Native Federation (Standards-Based)

Angular's "Native Federation" uses ESM and import maps instead of Webpack. As browsers improve native ESM support and import map adoption grows, this could become the standards-based future of MFE — bundler-agnostic by design.

5. Type Safety at the Boundary

Auto-generated .d.ts for federated modules is now table stakes. Expect better tooling around versioned type contracts, breaking-change detection, and dependency graphs across federated boundaries.


TL;DR

  • Micro frontends are an organizational tool, not a technical one. They exist because Conway's Law won.
  • Most teams should not use them. If you have fewer than 5 product teams, a clean monorepo is almost always the right answer.
  • Module Federation is the dominant runtime approach in 2026 — for React + Vite, for Next.js (alongside multi-zones), and for React Native (via Re.Pack).
  • Singletons for stateful libraries are not optional. Two Reacts on one page will ruin your week.
  • Independent deploys are the entire point. If you don't have them, you have a distributed monolith with extra steps.
  • Mobile MFE is hard mode. Use it only if you're building a true super app.
  • Test the boundaries with contract tests. Type the boundaries with auto-generated .d.ts.
  • Use the URL as the integration layer wherever you can. It eliminates 60% of the "shared state" problems.
  • Performance gets worse before it gets better — plan for bundle deduplication and aggressive caching from day one.
  • The right answer is usually the boring one. Multi-zones, monorepos, and a strong design system will solve most of the problems people reach for MFE to fix.

If you walked away from this guide thinking "maybe we don't need micro frontends after all", I've done my job.

If you walked away thinking "okay, we definitely do, and now I know how to start" — even better. You're going in with your eyes open.


Connect With Me

If this guide helped you, follow me on LinkedIn for more deep dives into frontend architecture, system design, and the trade-offs nobody talks about: linkedin.com/in/ishaanpandey

Got questions, war stories, or strong opinions about whether your team should adopt MFE? I want to hear them. Drop me a message.

Top comments (0)