React Native Monorepo mit Turborepo: mehrere Apps verwalten
AI generated
RN
native
React Native · Turborepo · Monorepo · Tooling
React Native Monorepo mit Turborepo
mehrere Apps in einem Repository verwalten

Ein React Native Monorepo mit Turborepo bündelt mehrere Apps, geteilte Packages und Build-Caching in einem einzigen Repository. Dieser Artikel zeigt den Aufbau mit pnpm Workspaces, die Turborepo-Pipeline-Konfiguration und die Metro-Anpassungen, die für ein stabiles Monorepo notwendig sind.

17 Min. Lesezeit pnpm Workspaces · Turborepo · Metro · EAS Build React Native 0.74+

1. Warum ein Monorepo für mehrere React-Native-Apps

Teams, die eine Kunden-App und eine Partner-App betreiben, oder mehrere White-Label-Varianten derselben Basis pflegen, stehen früher oder später vor derselben Frage: getrennte Repositories mit kopiertem Code, oder ein React Native Monorepo, das alle Apps und den geteilten Code an einem Ort hält. Kopierter Code driftet auseinander, ein Bugfix in der einen App landet selten synchron in der anderen, und Design-System-Änderungen müssen doppelt gepflegt werden.

Ein Monorepo löst dieses Problem strukturell: UI-Komponenten, API-Clients und Konfigurationspresets liegen einmal in geteilten Packages, jede App importiert daraus. Änderungen wirken sich sofort auf alle Apps aus, Versionskonflikte zwischen Repos entfallen, und Code-Reviews sehen abteilungsübergreifende Auswirkungen einer Änderung in einem einzigen Pull Request.

Der Preis dafür ist Tooling-Komplexität: Ohne einen Task-Runner wie Turborepo baut jeder CI-Lauf sämtliche Apps neu, selbst wenn nur eine einzige Datei in einer einzigen App geändert wurde. Genau hier setzt die Kombination aus Workspace-Manager und Turborepo an, um ein React Native Monorepo praktikabel zu halten.

2. Workspace-Grundlagen: pnpm, apps/ und packages/

Die Basis jedes Monorepos ist der Workspace-Manager. pnpm Workspaces sind für React-Native-Projekte die verbreitetste Wahl, weil pnpm durch seinen Content-addressable Store Festplattenspeicher spart und striktere Node-Modules-Auflösung erzwingt als npm oder yarn Classic. Yarn Workspaces sind eine gleichwertige Alternative, wenn ein Team bereits auf Yarn setzt.

Die Ordnerkonvention hat sich über Projekte hinweg etabliert: apps/ enthält die eigenständigen React-Native-Anwendungen, packages/ die geteilten Bibliotheken. Jede App und jedes Package hat eine eigene package.json, referenziert geteilte Packages aber nicht über eine registrierte npm-Version, sondern über das workspace-Protokoll, das lokal auf den Ordner im selben Repository verweist.

Dieses workspace-Protokoll ist der entscheidende Unterschied zu getrennten Repositories mit einem privaten npm-Registry: Änderungen in einem Package sind sofort in allen Apps sichtbar, ohne Publish-Schritt und ohne Versionsnummer-Update. Für ein React Native Monorepo mit häufigen Änderungen am Design-System ist das ein erheblicher Geschwindigkeitsvorteil gegenüber einem Multi-Repo-Setup.


# Initialize a pnpm workspace at the repository root
pnpm init

# pnpm-workspace.yaml defines which folders are workspace packages
cat > pnpm-workspace.yaml << 'YAML'
packages:
  - "apps/*"
  - "packages/*"
YAML

# Create two React Native apps and one shared UI package
mkdir -p apps/consumer-app apps/partner-app packages/ui packages/config

# Install a shared package into an app using the workspace protocol
pnpm --filter consumer-app add @acme/ui@workspace:*

Wie sich das workspace-Protokoll konkret in der package.json einer App niederschlägt, zeigt das folgende Beispiel. Statt einer festen Versionsnummer wie 1.2.0 steht dort ein Verweis, der von pnpm zur Installationszeit auf den lokalen Package-Ordner aufgelöst wird, unabhängig davon, ob dieses Package jemals bei npm veröffentlicht wurde.


{
  "name": "consumer-app",
  "version": "1.4.0",
  "dependencies": {
    "@acme/ui": "workspace:*",
    "@acme/api": "workspace:*",
    "@acme/config": "workspace:*",
    "react-native": "0.75.2"
  }
}

Ein weiterer Vorteil dieser Struktur zeigt sich beim Onboarding neuer Teammitglieder in einem React Native Monorepo: Ein einziger pnpm install im Repository-Root installiert sämtliche Abhängigkeiten für alle Apps und Packages gleichzeitig, inklusive korrekter Verlinkung der internen Packages untereinander. Es gibt keinen zusätzlichen Schritt, um lokale Packages manuell zu verknüpfen, wie es bei separaten Repositories mit npm link oft nötig wäre.

3. Turborepo-Grundlagen: Pipeline und Task-Graph

Turborepo ergänzt den Workspace-Manager um einen Task-Runner, der Abhängigkeiten zwischen Tasks versteht und Ergebnisse cached. Die zentrale Konfigurationsdatei turbo.json definiert Pipelines: Welcher Task hängt von welchem anderen Task ab, welche Outputs sollen gecacht werden, und welche Umgebungsvariablen beeinflussen das Ergebnis eines Tasks.

Der Operator dependsOn mit dem Präfix ^ (etwa ^build) bedeutet, dass ein Task erst läuft, nachdem derselbe Task in allen Packages abgeschlossen ist, von denen das aktuelle Package abhängt. Für ein React Native Monorepo heißt das konkret: Der build-Task einer App wartet automatisch darauf, dass das ui-Package und das config-Package zuerst gebaut wurden, ohne dass das manuell in einem Skript orchestriert werden muss.

Caching ist der eigentliche Geschwindigkeitsgewinn: Turborepo hasht die Eingabedateien eines Tasks und überspringt die Ausführung komplett, wenn sich seit dem letzten Lauf nichts geändert hat, inklusive Wiederverwendung über verschiedene Branches und Entwickler hinweg bei aktivem Remote Caching. Ein CI-Lauf, der zuvor zehn Minuten brauchte, kann bei unveränderten Packages auf wenige Sekunden schrumpfen.


{
  "$schema": "https://turbo.build/schema.json",
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**", ".expo/**"]
    },
    "lint": {
      "dependsOn": ["^build"]
    },
    "test": {
      "dependsOn": ["^build"],
      "outputs": ["coverage/**"]
    },
    "dev": {
      "cache": false,
      "persistent": true
    }
  }
}

4. Code teilen: UI-, Utils- und Config-Packages

Ein typisches React Native Monorepo teilt Code über mindestens drei Package-Kategorien. Ein ui-Package enthält die Design-System-Komponenten, Buttons, Karten, Formularelemente, die visuell konsistent über alle Apps hinweg aussehen sollen. Ein utils- oder api-Package bündelt den API-Client, Authentifizierungslogik und geteilte Hooks. Ein config-Package hält ESLint-, TypeScript- und Tailwind-Presets, damit jede App dieselben Qualitätsregeln erbt, ohne sie einzeln zu pflegen.

Jedes dieser Packages exportiert eine klar definierte öffentliche API über seine package.json main- und exports-Felder, statt interne Implementierungsdateien direkt zugänglich zu machen. Das verhindert, dass Apps versehentlich an interne Details eines Packages koppeln, die sich später ohne Vorwarnung ändern könnten.

Für ein Monorepo mit mehreren React-Native-Apps ist das ui-Package meist der Bereich mit dem größten Wartungsaufwand, weil Styling-Entscheidungen (Farben, Abstände, Typografie-Skalen) direkt über Tailwind- beziehungsweise NativeWind-Konfiguration im config-Package verankert werden sollten, statt in jeder App einzeln definiert zu werden.


// apps/consumer-app/screens/ProfileScreen.jsx
import { Button, Card } from '@acme/ui';
import { useCurrentUser } from '@acme/api';

export function ProfileScreen() {
  const { user, isLoading } = useCurrentUser();

  if (isLoading) return null;

  return (
    <Card>
      <Button label={"Edit profile for " + user.name} onPress={() => {}} />
    </Card>
  );
}

5. Metro-Konfiguration für Monorepos

Metro, der Bundler von React Native, geht standardmäßig davon aus, dass alle relevanten Dateien innerhalb eines einzigen Projektordners liegen. In einem React Native Monorepo muss Metro dagegen sowohl den Ordner der App als auch die Repository-Wurzel und die geteilten Packages beobachten, sonst werden Änderungen an einem importierten Package nicht als Datei-Update erkannt.

Die Metro-Konfiguration jeder App erweitert deshalb watchFolders um die Repository-Wurzel und passt resolver.nodeModulesPaths an, damit Metro node_modules sowohl im App-Ordner als auch im gemeinsamen Wurzel-node_modules durchsucht, das durch das pnpm-Workspace-Hoisting entsteht. Ein häufiger Stolperstein ist die doppelte Auflösung von React oder React Native selbst, wenn mehrere Kopien in verschachtelten node_modules landen, was zu kryptischen Laufzeitfehlern über "invalid hook call" führt.

Die zuverlässigste Gegenmaßnahme ist, React und React Native explizit als einzige Instanz über resolver.extraNodeModules auf den Root-node_modules-Pfad zu zwingen, statt sich auf die automatische Hoisting-Logik von pnpm zu verlassen. Diese Konfiguration ist in fast jedem produktiven React Native Monorepo notwendig, sobald mehr als eine App existiert.

6. Native Code im Monorepo-Kontext

Ein wichtiger Unterschied zu reinen Web-Monorepos: Jede React-Native-App behält trotz geteilter JavaScript-Packages ihre eigenen nativen ios/- und android/-Projekte. Ein geteiltes ui-Package kann zwar JavaScript-Komponenten bereitstellen, aber wenn es selbst native Module enthält, muss jede App diese über Autolinking in ihrem eigenen Podfile und ihrer eigenen Gradle-Konfiguration einbinden.

Für Podfile und Gradle bedeutet das konkret: Die relativen Pfade zu react-native-Skripten müssen auf die Position im Monorepo angepasst werden, da die App nicht mehr direkt im Repository-Root liegt. Die meisten aktuellen React-Native-Vorlagen berücksichtigen das bereits über eine node --print-Auflösung, die den tatsächlichen Installationspfad von react-native unabhängig von der Verzeichnistiefe ermittelt.

Autolinking selbst funktioniert im React Native Monorepo genauso wie in einem Einzelprojekt, solange die native Bibliothek als Abhängigkeit der jeweiligen App aufgelöst wird, nicht nur des geteilten Packages. Ein häufiger Fehler ist, eine native Bibliothek ausschließlich im ui-Package zu deklarieren, ohne sie auch als transitive Abhängigkeit in der App selbst sichtbar zu machen, was zu fehlender Autolinking-Erkennung führt.

Bei zwei Apps mit unterschiedlichem nativen Funktionsumfang, etwa wenn nur die Partner-App eine Kamera-Bibliothek benötigt, bleibt die Kontrolle über native Abhängigkeiten pro App vollständig erhalten. Das Monorepo zwingt keine App dazu, native Module zu bündeln, die sie gar nicht verwendet, solange die package.json jeder App präzise nur die tatsächlich benötigten nativen Bibliotheken auflistet.

7. Remote Caching: schnellere CI über Branches hinweg

Lokales Caching beschleunigt wiederholte Builds auf derselben Maschine, aber der eigentliche Produktivitätsgewinn eines React Native Monorepos entsteht durch Remote Caching. Vercel Remote Cache oder ein selbst gehosteter Cache-Server erlauben es, Task-Ergebnisse zwischen CI-Läufen, Branches und sogar zwischen den lokalen Maschinen verschiedener Entwickler zu teilen.

Praktisch bedeutet das: Wenn ein Entwickler A ein Package bereits lokal gebaut und den Cache hochgeladen hat, muss Entwickler B denselben Build nicht erneut ausführen, Turborepo lädt das gecachte Ergebnis stattdessen herunter. Für CI-Pipelines mit vielen parallelen Feature-Branches summiert sich dieser Effekt zu spürbar kürzeren Wartezeiten bis zum grünen Build.

Cache-Tokens für Remote Caching sind sicherheitskritisch und sollten wie jedes andere CI-Secret behandelt werden: Ein kompromittierter Token erlaubt potenziell das Einschleusen manipulierter Build-Artefakte in den Cache. Rotierende Tokens und ein separater, weniger privilegierter Token für Pull-Request-Pipelines reduzieren dieses Risiko in einem Monorepo mit externen Beitragenden.

8. Versionierung und Release-Strategie

Apps in einem React Native Monorepo haben typischerweise unabhängige Versionsnummern und Release-Zyklen, während geteilte Packages am besten über Changesets versioniert werden, ein Tool, das Änderungen an Packages erfasst und daraus automatisch Versionsbumps und Änderungsprotokolle generiert. Das verhindert, dass ein Package versehentlich mit inkompatiblen Breaking Changes in mehrere Apps gleichzeitig einfließt, ohne dass das sichtbar dokumentiert wurde.

EAS Build fügt sich in ein Monorepo-Setup über eine pro App konfigurierte eas.json ein, wobei das Working Directory explizit auf den jeweiligen App-Ordner gesetzt werden muss, damit der Build-Server weiß, welche App tatsächlich gepackt werden soll. Ohne diese Konfiguration versucht EAS Build sonst, das gesamte Repository als eine einzelne App zu interpretieren.

Für Teams mit White-Label-Varianten empfiehlt sich zusätzlich eine klare Konvention, welche App-spezifischen Umgebungsvariablen wo gepflegt werden, damit ein Build-Fehler in der Partner-App nicht versehentlich Konfiguration aus der Consumer-App verwendet.

9. CI/CD-Integration mit gefilterten Tasks

Ohne Filterung baut eine CI-Pipeline in einem React Native Monorepo bei jedem Commit sämtliche Apps neu, selbst wenn nur eine einzige Datei in einer einzigen App geändert wurde. Turborepos --filter-Flag löst dieses Problem, indem es nur die Tasks der tatsächlich betroffenen Packages ausführt, basierend auf dem Abhängigkeitsgraphen und den geänderten Dateien seit dem letzten gemeinsamen Commit mit dem Ziel-Branch.

Eine typische CI-Konfiguration splittet Jobs pro App auf, jeder Job ruft turbo run build --filter=consumer-app... auf, wobei die drei Punkte auch alle Packages einschließen, von denen consumer-app transitiv abhängt. Das reduziert die CI-Laufzeit erheblich, insbesondere in Monorepos mit vier oder mehr Apps, von denen bei einem typischen Pull Request meist nur eine einzige tatsächlich betroffen ist.

Die größte Fallgrube bei dieser Filterung ist ein falsch konfigurierter Basis-Branch für den Diff-Vergleich: Wird der Vergleichspunkt falsch gesetzt, baut Turborepo entweder zu viel (Performance-Verlust) oder zu wenig (fehlerhafter Build, der eine Änderung übersieht). Beide Fälle lassen sich mit turbo run build --filter=...[origin/main] zuverlässig vermeiden, sofern die CI-Umgebung Zugriff auf die vollständige Git-Historie hat.

Kriterium Getrennte Repositories React Native Monorepo mit Turborepo
Code-Teilung Copy-Paste oder private npm-Registry Direkter Import über workspace-Protokoll
CI-Laufzeit Pro Repo isoliert, aber redundant Gefiltert und gecacht, nur Betroffenes baut
Design-System-Updates Manuell in jedem Repo nachziehen Sofort in allen Apps sichtbar
Onboarding-Aufwand Ein Repo pro Aufgabe, einfacher Einstieg Mehr initiale Tooling-Komplexität
Versionskonsistenz Divergiert leicht über Zeit Eine Quelle der Wahrheit pro Package

# CI job: only build and test what the changed files actually affect
# Compares against the main branch merge base for an accurate diff

pnpm install --frozen-lockfile

# Build only the consumer-app and its dependency graph
pnpm turbo run build --filter=consumer-app...[origin/main]

# Run tests only for packages affected since the base branch
pnpm turbo run test --filter=...[origin/main]

Mironsoft

React Native Architektur und Monorepo-Tooling für Multi-App-Teams

Mehrere Apps, ein Repository, ohne CI-Chaos?

Wir bauen React-Native-Monorepos mit Turborepo, sauberen Package-Grenzen und CI-Pipelines, die nur bauen, was sich wirklich geändert hat.

Monorepo-Setup

pnpm Workspaces, Turborepo-Pipelines und Metro-Konfiguration

Package-Architektur

UI-, API- und Config-Packages mit klaren öffentlichen Schnittstellen

CI-Optimierung

Remote Caching und gefilterte Pipelines für kürzere Build-Zeiten

10. Zusammenfassung

Ein React Native Monorepo mit Turborepo löst das strukturelle Problem mehrerer Apps mit geteiltem Code, ohne die typischen Nachteile getrennter Repositories in Kauf zu nehmen. pnpm Workspaces stellen die Grundlage über apps/ und packages/ bereit, das workspace-Protokoll verbindet Apps und geteilte Packages ohne Publish-Schritt. Turborepo orchestriert Task-Abhängigkeiten und cached Ergebnisse, sodass ein CI-Lauf nur die tatsächlich betroffenen Teile neu baut.

Die kritischen Stolpersteine liegen abseits der offensichtlichen Konfiguration: eine korrekte Metro-Konfiguration gegen doppelte React-Instanzen, native Autolinking-Pfade, die auf die tatsächliche Position im Monorepo angepasst sind, und ein sauberer Basis-Branch-Vergleich für gefilterte CI-Tasks. Wer diese drei Punkte von Anfang an berücksichtigt, vermeidet die häufigsten Ursachen für kryptische Fehler in produktiven Multi-App-Monorepos.

Für Teams, die heute noch mit einer einzigen App starten, aber mittelfristig eine zweite Variante planen, lohnt es sich, die Monorepo-Struktur bereits beim ersten Projekt anzulegen, statt später eine Migration nachzuholen. Der Mehraufwand für apps/ und packages/ von Anfang an ist gering, das Nachrüsten eines bestehenden Einzelprojekts dagegen deutlich aufwendiger.

React Native Monorepo mit Turborepo: Das Wichtigste auf einen Blick

Workspace-Struktur

apps/ für eigenständige Apps, packages/ für geteilten Code, verbunden über das workspace-Protokoll.

Turborepo-Pipeline

turbo.json definiert Task-Abhängigkeiten über dependsOn und cached Outputs für schnellere wiederholte Läufe.

Metro-Anpassung

watchFolders und extraNodeModules verhindern doppelte React-Instanzen und Auflösungsfehler.

CI-Filterung

--filter mit korrektem Basis-Branch-Vergleich baut nur tatsächlich betroffene Apps und Packages.

11. FAQ: React Native Monorepo mit Turborepo

1Ab wann lohnt sich ein Monorepo?
Sobald mindestens zwei Apps signifikant Code teilen, etwa Design-System oder API-Client.
2pnpm oder Yarn Workspaces?
pnpm wegen Content-addressable Store und strikterer Auflösung ist verbreiteter, Yarn gleichwertig.
3Was bedeutet ^build?
Task läuft erst, wenn derselbe Task in allen abhängigen Packages abgeschlossen ist.
4invalid hook call im Monorepo?
Meist doppelte React-Instanzen, extraNodeModules auf Root-Instanz erzwingen behebt es.
5Eigene native Projekte pro App?
Ja, jede App behält eigene ios/ und android/ Ordner trotz geteiltem JavaScript.
6Nutzen von Remote Caching?
Task-Ergebnisse werden über CI-Läufe, Branches und Entwickler-Maschinen geteilt und übersprungen bei Unverändert.
7Versionierung geteilter Packages?
Changesets erfasst Änderungen und generiert automatisch Versionsbumps und Änderungsprotokolle.
8EAS Build im Monorepo?
Über pro App konfigurierte eas.json mit explizitem Working Directory.
9Alle Apps bei jedem Commit vermeiden?
--filter=app-name...[origin/main] baut nur tatsächlich betroffene Packages und Apps.
10Turborepo oder Nx?
Turborepo ist leichtgewichtiger, Nx bietet mehr Generatoren. Für die meisten Teams reicht Turborepo.