</>
{ }
React · Micro-Frontends · Module Federation · Webpack 5
React Micro-Frontends mit
Module Federation 2.0

Wer eine wachsende React-Anwendung als Monolith betreibt, kämpft irgendwann mit langen Build-Zeiten, Deployment-Abhängigkeiten und Team-Konflikten. Module Federation 2.0 löst diese Probleme durch echte Laufzeit-Komposition: Teams deployen ihre App-Teile unabhängig, und die Shell-App komponiert alles dynamisch zusammen.

18 Min. Lesezeit Shell-App · Remote · Shared Deps · TypeScript · Vite React 18 · Webpack 5 · Module Federation 2.0

1. Was Micro-Frontends wirklich lösen

Das Kernproblem großer Frontend-Projekte ist nicht fehlende Technologie, sondern organisatorische Kopplung. Wenn zehn Teams an derselben React-Codebase arbeiten, wird jede Änderung zur Koordinationsaufgabe. Deployments brauchen alle Beteiligten, ein fehlerhafter PR blockiert alle anderen, und Build-Zeiten explodieren, weil jede CI-Pipeline das gesamte Projekt kompiliert. Micro-Frontends lösen das organisatorische Problem, bevor es ein technisches wird: Jedes Team besitzt seinen Teil der Anwendung vollständig – Code, Tests, Deployment-Pipeline.

Module Federation ist nicht einfach Code-Splitting. Beim klassischen Code-Splitting entscheidet der Build-Prozess, welche Chunks entstehen. Bei Module Federation entscheidet der Laufzeit-Loader, welche Remote-Apps geladen werden — und woher. Das bedeutet: Remote-App-Team und Shell-App-Team können vollständig unabhängig deployen. Die Shell fragt zur Laufzeit nach dem aktuellen Stand der Remote, nicht nach dem Stand zum Build-Zeitpunkt. Das ist der fundamentale Unterschied gegenüber allen Build-Zeit-Ansätzen wie NPM-Packages oder Monorepo-Builds.

2. Module Federation 2.0 — Konzepte und Neuerungen

Module Federation 1.0 kam mit Webpack 5 und ermöglichte erstmals echte Laufzeit-Komposition von JavaScript-Modulen über App-Grenzen hinweg. Version 2.0, veröffentlicht als @module-federation/enhanced, bringt mehrere wichtige Verbesserungen: ein verbessertes Typsystem für Remote-Exports, ein Runtime-Plugin-System, das Verhalten ohne Re-Build anpassen kann, und eine Vite-kompatible Implementierung über @originjs/vite-plugin-federation. Neu ist auch die offizielle Unterstützung für dynamische Remote-Registrierung zur Laufzeit ohne vorherige Konfiguration im Build.

Die Grundkonzepte bleiben: Ein Host (Shell-App) konsumiert Remotes. Ein Remote exponiert Komponenten oder Module über eine remoteEntry.js-Datei. Shared definiert, welche Dependencies (vor allem React und React-DOM) zwischen allen Apps geteilt werden, damit jede nur einmal im Browser läuft. Module Federation 2.0 führt zusätzlich Manifest-Dateien ein, die es der Shell erlauben, verfügbare Remotes zur Laufzeit zu entdecken, ohne statische URLs im Build zu haben. Das vereinfacht dynamische Skalierung erheblich.

3. Shell-App aufsetzen: Host-Konfiguration

Die Shell-App ist der Entry-Point für den Nutzer. Sie lädt die globale Navigation, das Routing und — dynamisch zur Laufzeit — die einzelnen Remote-Apps. Die Webpack-Konfiguration der Shell nutzt ModuleFederationPlugin als Host und definiert alle bekannten Remotes mit ihren URLs. In Produktionsumgebungen kommen diese URLs aus Umgebungsvariablen oder einem Service-Discovery-Endpoint, nicht aus dem Build selbst. Die Shell selbst enthält keinen fachlichen Code der Remote-Teams — sie ist reines Scaffolding.

Ein kritisches Detail: Die Shell muss ihre eigene App asynchron booten, damit das Shared-Dependencies-System des Module-Federation-Runtimes vollständig initialisiert ist, bevor irgendetwas gerendert wird. Der übliche Trick ist ein bootstrap.tsx, das dynamisch importiert wird, während index.ts nur diese eine dynamische Import-Zeile enthält. Wer das vergisst, bekommt zur Laufzeit Fehler, weil React mehrfach geladen wird — einmal von der Shell, einmal von einer Remote.


// webpack.config.ts — Shell-App (Host) configuration
import { ModuleFederationPlugin } from '@module-federation/enhanced/webpack';

export default {
  plugins: [
    new ModuleFederationPlugin({
      name: 'shell',
      // Remote URLs come from environment variables at runtime
      remotes: {
        catalogApp: `catalogApp@${process.env.CATALOG_URL}/remoteEntry.js`,
        checkoutApp: `checkoutApp@${process.env.CHECKOUT_URL}/remoteEntry.js`,
        accountApp: `accountApp@${process.env.ACCOUNT_URL}/remoteEntry.js`,
      },
      shared: {
        react: { singleton: true, requiredVersion: '^18.0.0' },
        'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
        'react-router-dom': { singleton: true, requiredVersion: '^6.0.0' },
      },
    }),
  ],
};

// src/index.ts — async bootstrap trick (REQUIRED for shared deps)
import('./bootstrap');

// src/bootstrap.tsx — actual app entry
import React from 'react';
import { createRoot } from 'react-dom/client';
import { ShellApp } from './ShellApp';

const root = createRoot(document.getElementById('root')!);
root.render(<ShellApp />);

4. Remote-App erstellen und exponieren

Jede Remote-App ist eine eigenständige React-Anwendung — sie kann lokal entwickelt, getestet und deployed werden, ohne dass die Shell existieren muss. Die Webpack-Konfiguration der Remote definiert, welche Komponenten oder Module sie nach außen freigibt. Das Schlüsselwort ist exposes: Es ist ein Mapping von öffentlichem Namen zu internem Dateipfad. Die Remote-App baut dann eine remoteEntry.js-Datei, die der Host zur Laufzeit laden kann.

Wichtig ist, dass exponierte Komponenten keine Annahmen über den Kontext treffen, in dem sie laufen. Sie sollten keine globalen CSS-Variablen erwarten, die nur in ihrer eigenen App definiert sind, und keinen globalen State aus einer Store-Instanz, die vielleicht nicht existiert. Stattdessen erhalten sie alles was sie brauchen über Props oder über explizit geteilte Dependencies (wie einen gemeinsamen React-Query-Client). Gut definierte Schnittstellen — am besten mit TypeScript und exportierten Props-Typen — sind das Fundament stabiler Micro-Frontend-Integration.


// webpack.config.ts — Catalog Remote-App configuration
import { ModuleFederationPlugin } from '@module-federation/enhanced/webpack';

export default {
  plugins: [
    new ModuleFederationPlugin({
      name: 'catalogApp',
      filename: 'remoteEntry.js',
      // Public interface: what the shell can import from this remote
      exposes: {
        './ProductList': './src/components/ProductList',
        './ProductDetail': './src/components/ProductDetail',
        './CategoryNav': './src/components/CategoryNav',
      },
      shared: {
        react: { singleton: true, requiredVersion: '^18.0.0' },
        'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
      },
    }),
  ],
};

// src/components/ProductList.tsx — exposed component (standalone)
import React from 'react';

interface ProductListProps {
  categoryId: string;
  onProductSelect: (productId: string) => void;
}

// Clean props interface — no assumptions about parent context
export const ProductList: React.FC<ProductListProps> = ({
  categoryId,
  onProductSelect,
}) => {
  // Component fetches its own data — fully autonomous
  return <div className="catalog-product-list">{/* ... */}</div>;
};

export default ProductList;

5. Shared Dependencies: React nur einmal laden

Das größte Risiko bei Micro-Frontends ist das mehrfache Laden von React. Wenn Shell-App und Remote-App unterschiedliche React-Instanzen haben, sind Hook-Aufrufe aus der Remote-App aus Sicht der Shell-App Fremdobjekte — das führt zu dem berüchtigten Fehler "Invalid hook call". Das shared-Feld in der Module-Federation-Konfiguration verhindert das: Es teilt denselben React-Bundle zwischen allen Apps, die ihn deklarieren. singleton: true erzwingt, dass immer nur eine Instanz existiert — auch wenn verschiedene Remotes unterschiedliche Minor-Versionen angeben.

Versionskonflikte sind dabei das häufigste Operationsproblem. Wenn die Shell React 18.2 will und eine Remote nur React 18.1 liefert, entscheidet die Singleton-Konfiguration, welche Version gewinnt. Durch requiredVersion kann man Mindestversionen erzwingen und bekommen bei Verletzung zur Laufzeit eine Warnung in der Browser-Konsole. Für den Produktionsbetrieb empfiehlt sich ein zentrales shared-deps-Paket im Monorepo, das die Versionen für alle Apps vorgibt — so bleibt die Konfiguration konsistent, ohne Copy-Paste zwischen mehreren webpack.config.ts-Dateien.

6. Typsichere Remotes mit TypeScript

Das klassische Problem mit Module Federation und TypeScript: Die Shell importiert eine Komponente aus einer Remote mit import('catalogApp/ProductList'), aber TypeScript kennt den Typ dieser Komponente nicht — es gibt keine node_modules/catalogApp zum Zeitpunkt der Kompilierung der Shell. Module Federation 2.0 löst das mit automatisch generierten Typ-Deklarationsdateien. Der Remote-Build erzeugt eine @mf-types.d.ts, die die Shell-App einbinden kann. Die Typen bleiben damit immer synchron mit dem tatsächlichen Export der Remote — ohne manuelles Schreiben von Deklarationen.

Für Teams, die noch nicht Module Federation 2.0 nutzen, gibt es den bewährten Workaround: Ein gemeinsames @types/remotes-Paket im Monorepo, das alle Remote-Schnittstellen als TypeScript-Declarations enthält. Wenn sich eine Remote-Schnittstelle ändert, wird dieses Paket aktualisiert — die Shell-App bekommt beim nächsten Typ-Check sofort Fehlermeldungen, wenn sie eine veraltete API nutzt. So bleibt die Typsicherheit gewahrt, auch wenn Runtime und Compile-Time getrennte Welten sind.

7. Routing über Micro-Frontend-Grenzen hinweg

Routing ist eine der kompliziertesten Fragen in Micro-Frontend-Architekturen. Das empfohlene Muster mit React Router 6: Die Shell-App besitzt die Top-Level-Routen und rendert für jeden Pfad-Prefix die zuständige Remote-App. Die Remote-App bekommt einen basename-Props, der ihr mitteilt, welchen Pfad-Prefix sie verwaltet. Intern nutzt die Remote-App dann ihren eigenen Router — vollständig unabhängig von der Shell. Navigationen innerhalb der Remote bleiben lokal; Navigationen zu anderen Micro-Frontends gehen über die Shell.

Für Cross-Remote-Navigation gibt es zwei Strategien: Entweder nutzt man das native Browser-Routing mit window.history.pushState, das beide Apps über popstate-Events abhören. Oder man definiert einen gemeinsamen Event-Bus als Shared Dependency — eine einfache EventEmitter-Instanz, über die Remotes Navigationsabsichten kommunizieren, ohne direkt voneinander zu wissen. Letzteres ist expliziter und testbarer, erfordert aber einen definierten Kontrakt zwischen den Teams.


// Shell: lazy-load remote components with Suspense + ErrorBoundary
import React, { Suspense, lazy } from 'react';
import { Routes, Route } from 'react-router-dom';
import { ErrorBoundary } from './components/ErrorBoundary';

// Dynamic imports from remote apps — resolved at runtime
const ProductList = lazy(() => import('catalogApp/ProductList'));
const ProductDetail = lazy(() => import('catalogApp/ProductDetail'));
const Checkout = lazy(() => import('checkoutApp/CheckoutFlow'));
const Account = lazy(() => import('accountApp/AccountDashboard'));

const RemoteFallback = () => (
  <div className="remote-loading">Anwendungsbereich wird geladen…</div>
);

export const ShellRouter: React.FC = () => (
  <Routes>
    <Route
      path="/catalog/*"
      element={
        <ErrorBoundary fallback={<div>Katalog nicht verfügbar</div>}>
          <Suspense fallback={<RemoteFallback />}>
            <ProductList categoryId="root" onProductSelect={() => {}} />
          </Suspense>
        </ErrorBoundary>
      }
    />
    <Route
      path="/checkout/*"
      element={
        <ErrorBoundary fallback={<div>Checkout nicht verfügbar</div>}>
          <Suspense fallback={<RemoteFallback />}>
            <Checkout />
          </Suspense>
        </ErrorBoundary>
      }
    />
  </Routes>
);

8. Fehlerbehandlung und Fallbacks

Ein Micro-Frontend-System muss davon ausgehen, dass einzelne Remotes temporär nicht erreichbar sind. Das ist kein Ausnahmezustand, sondern normaler Betrieb in einem verteilten System. React's ErrorBoundary ist die erste Verteidigungslinie: Wenn eine Remote-Komponente beim Laden oder Rendern wirft, fängt die ErrorBoundary den Fehler und rendert einen Fallback. Ohne ErrorBoundary würde ein Remote-Fehler die gesamte Shell-App abschießen — ein inakzeptabler Ausfall für einen Dienst, der eigentlich nur einen kleinen Teil der App betrifft.

Für das Laden der remoteEntry.js selbst bietet Module Federation 2.0 eine Retry-Logik und die Möglichkeit, alternative URLs zu konfigurieren. In der Praxis empfiehlt sich ein Monitoring auf Remote-Ladezeiten: Wenn eine remoteEntry.js mehr als eine Sekunde zum Laden braucht, ist das ein frühes Warnsignal für Deployment-Probleme beim Remote-Team. Alerting auf diese Metriken verhindert, dass ein Remote-Deployment erst durch Nutzerbeschwerden auffällt.

9. Ansätze im direkten Vergleich

Es gibt mehrere Ansätze, um eine React-Anwendung in unabhängig deploybare Teile aufzuteilen. Module Federation ist der mächtigste, aber auch der komplexeste. Die Wahl hängt von Team-Größe, Deployment-Frequenz und der tolerierten Komplexität ab.

Ansatz Deployment Typsicherheit Empfehlung
Module Federation 2.0 Vollständig unabhängig Automatisch via @mf-types Ab 3+ Teams, hohe Deploy-Frequenz
NPM-Packages (Monorepo) Build-Zeit-Kopplung Vollständig 1–2 Teams, seltene Releases
iframe-Komposition Unabhängig Keine Nur für Legacy-Integration
Web Components Unabhängig Begrenzt Framework-unabhängige Remotes
Single-SPA Unabhängig Manuell Mixed-Framework-Umgebungen

Module Federation gewinnt eindeutig bei der Deployment-Unabhängigkeit und der Integration in den normalen React-Entwicklungsworkflow — Remote-Komponenten sehen aus wie normale React-Komponenten, nur mit einem anderen Import-Pfad. Der Preis ist die Build-Komplexität: zwei Webpack-Konfigurationen, zwei CI-Pipelines, und die Notwendigkeit, Shared-Dependency-Versionen koordiniert zu pflegen. Für kleine Teams ohne echten Deploymentdruck ist ein Monorepo mit NPM-Packages oft die bessere Wahl.

Mironsoft

React-Architektur, Micro-Frontends und Module Federation

React-App in Micro-Frontends aufteilen?

Wir analysieren eure bestehende React-Architektur, definieren sinnvolle Schnittgrenzen und implementieren Module Federation — mit typsicherer Integration und CI-Pipeline pro Team.

Architektur-Review

Schnittgrenzen definieren, Shared-Dependencies analysieren, Team-Struktur evaluieren

Implementierung

Shell-App, Remote-Apps und Typ-Sharing mit Module Federation 2.0 aufbauen

CI/CD pro Team

Unabhängige Deployment-Pipelines mit Typ-Synchronisation und Integration-Tests

10. Zusammenfassung

React Micro-Frontends mit Module Federation 2.0 lösen das organisatorische Skalierungsproblem großer Frontend-Projekte: Teams deployen unabhängig, Typen bleiben dank automatisch generierter Declarations synchron, und die Shell-App komponiert alles zur Laufzeit ohne Build-Zeit-Kopplung. Die Shell nutzt singleton: true für Shared Dependencies, damit React genau einmal im Browser existiert. Remote-Apps exponieren saubere, Props-basierte Schnittstellen ohne Annahmen über den Kontext. ErrorBoundaries und Suspense-Fallbacks halten die Shell stabil, wenn ein Remote temporär nicht erreichbar ist.

Der Weg zur produktionsreifen Micro-Frontend-Architektur ist iterativ: Zuerst die Shell aufsetzen, eine Remote migrieren, den Deployment-Prozess etablieren. Dann weitere Remotes nach und nach auslagern, wenn das Team den Workflow versteht. Das größte Risiko ist nicht die Technologie, sondern eine unklare Schnittgrenze — ein Remote, der zu viel vom Kontext der Shell erwartet, ist genauso gekoppelt wie ein Monolith, nur schwerer zu debuggen.

Module Federation 2.0 — Das Wichtigste auf einen Blick

Async Bootstrap

index.ts importiert nur bootstrap.tsx dynamisch — Pflicht, damit Shared Dependencies korrekt initialisiert werden, bevor React startet.

Singleton React

singleton: true in allen shared-Konfigurationen — verhindert doppeltes React und "Invalid hook call"-Fehler zur Laufzeit.

ErrorBoundary + Suspense

Jede Remote-Komponente in ErrorBoundary und Suspense wrappen — Remote-Ausfälle dürfen die Shell nie zum Absturz bringen.

Typen synchron halten

Module Federation 2.0 generiert @mf-types.d.ts automatisch. Alternativ: gemeinsames @types/remotes-Paket im Monorepo pflegen.

11. FAQ: React Micro-Frontends mit Module Federation 2.0

1Unterschied Module Federation vs. Code-Splitting?
Code-Splitting ist Build-Zeit. Module Federation ist Laufzeit — Remotes werden dynamisch geladen, vollständig unabhängig vom Build der Shell.
2Warum bootstrap.tsx dynamisch importieren?
Das Module-Federation-Runtime muss vor React initialisiert sein. Der dynamische Import in index.ts schafft den nötigen asynchronen Boundary.
3Remote nicht erreichbar — was passiert?
Ohne ErrorBoundary crasht die Shell. Mit ErrorBoundary rendert sie einen Fallback — der Rest der App bleibt voll funktionsfähig.
4React mehrfach laden verhindern?
In allen Apps react und react-dom mit singleton: true in der shared-Konfiguration. Das Runtime stellt sicher, dass nur eine Instanz existiert.
5Module Federation mit Vite möglich?
Ja. @originjs/vite-plugin-federation oder @module-federation/enhanced für Vite. MF 2.0 bietet bessere Typ-Integration.
6Wie testet man Micro-Frontend-Integration?
Unit-Tests in der Remote, Contract-Tests (Pact) für Schnittstellen, E2E-Tests (Playwright) für das zusammengesetzte System.
7Wann lohnt Module Federation nicht?
Bei einem Team oder seltenen Deployments ist ein Monorepo mit NPM-Packages einfacher und bietet dieselbe Typsicherheit ohne Laufzeit-Overhead.
8Shared-Dependency-Versionen koordinieren?
Ein zentrales shared-deps-Paket im Monorepo. Alle Apps importieren nur dieses — keine abweichenden Versionen durch Copy-Paste.
9Routing zwischen Remotes?
Shell besitzt Top-Level-Routen, Remote bekommt basename-Props und verwaltet Sub-Router intern. Cross-Remote via window.history oder Event-Bus.
10Was ist neu in Module Federation 2.0?
Automatische TypeScript-Declarations, Runtime-Plugin-System, Vite-Support und dynamische Remote-Registrierung. Paket: @module-federation/enhanced.