Code teilen zwischen React Web und React Native
AI generated
</>
{ }
React Native · Monorepo · Code-Sharing · Cross-Platform
Code teilen zwischen React Web und React Native
Monorepo-Strategien in der Praxis

Realistisches Code-Sharing zwischen React Web und React Native bedeutet nicht, dieselben Komponenten überall zu verwenden, sondern gezielt zu trennen, was plattformunabhängig ist, von dem, was es nicht sein kann. Business-Logik, Typen, API-Clients und Validierung lassen sich sauber teilen, Oberflächenkomponenten meist nicht. Dieser Artikel zeigt eine funktionierende Monorepo-Struktur mit echten Beispielen.

19 Min. Lesezeit Turborepo · pnpm Workspaces · Plattformdateien React 19 · React Native 0.76+

1. Was sich wirklich teilen lässt und was nicht

Der häufigste Irrtum beim Versuch, Code zwischen React Web und React Native zu teilen, ist die Annahme, komplette Oberflächenkomponenten ließen sich unverändert wiederverwenden. Das funktioniert nicht, weil beide Plattformen unterschiedliche Basiselemente verwenden: div und span im Web gegenüber View und Text in React Native. Wer versucht, diese Ebene zu teilen, produziert entweder Wrapper-Abstraktionen, die beide Welten schlechter bedienen als native Implementierungen, oder gibt das Vorhaben nach kurzer Zeit frustriert auf.

Realistisches Code-Sharing zielt stattdessen auf die Schichten unterhalb der Präsentation: Business-Logik, Zustandsverwaltung, API-Kommunikation, Validierungsregeln und TypeScript-Typen. Diese Schichten haben keinerlei Abhängigkeit zu View, div oder anderen plattformspezifischen Elementen und lassen sich zu nahezu 100 Prozent teilen. In gut strukturierten Projekten liegt der geteilte Code-Anteil oft bei 60 bis 80 Prozent der Gesamtlogik, während die tatsächliche Präsentationsschicht plattformspezifisch bleibt.

Diese Trennung ist keine Einschränkung, sondern folgt derselben Architektur, die auch ohne Cross-Platform-Ambitionen sinnvoll wäre: Eine klare Trennung zwischen Domänenlogik und Präsentation. Wer sein React-Web-Projekt bereits so strukturiert hat, findet beim Einstieg in React Native überraschend wenig Widerstand, weil die geteilten Bausteine bereits isoliert vorliegen.

2. Monorepo-Grundstruktur mit pnpm Workspaces

Die technische Grundlage für Code-Sharing ist fast immer ein Monorepo mit pnpm Workspaces oder Yarn Workspaces, orchestriert durch Turborepo für schnelle, gecachte Builds. Die Struktur trennt apps/ für die eigenständigen Anwendungen, also die Web-App und die React Native-App, von packages/ für geteilte Bibliotheken wie Business-Logik, Typen und Design-Tokens. Jedes Package im packages/-Ordner hat seine eigene package.json und kann unabhängig versioniert und getestet werden.

Turborepo cached Build- und Test-Ergebnisse pro Package, sodass ein Änderung in der React Native-App keinen erneuten Build der unveränderten Web-App auslöst. Bei wachsenden Projekten ist dieser Caching-Mechanismus keine Kür, sondern notwendig, um CI-Laufzeiten unter Kontrolle zu halten. Die Konfiguration in turbo.json definiert die Abhängigkeitsgraphen zwischen den Tasks, sodass etwa build in apps/mobile automatisch zuerst build in packages/shared-logic ausführt.


# Monorepo layout for sharing code between React web and React Native
myapp/
  apps/
    web/                  # React web app (Vite or Next.js)
    mobile/               # React Native app (Expo)
  packages/
    shared-logic/         # Business logic, hooks, validation
    shared-types/          # TypeScript types and API contracts
    api-client/            # Fetch wrapper and endpoint definitions
  package.json
  pnpm-workspace.yaml
  turbo.json

# Install and link all workspace packages
pnpm install

# Run mobile app dev server, shared packages auto-linked
pnpm --filter mobile dev

3. Geteilte Business-Logik und Custom Hooks

Custom Hooks ohne DOM-Abhängigkeit sind der ergiebigste Bereich für Code-Sharing zwischen React Web und React Native. Ein Hook, der Formularvalidierung implementiert, Warenkorb-Berechnungen durchführt oder Daten mit TanStack Query abfragt, enthält in der Regel keinen einzigen Verweis auf document, window oder ein natives Element. Solche Hooks wandern unverändert in ein geteiltes Package und werden von beiden Apps importiert.

Ein konkretes Beispiel ist ein Hook zur Warenkorbverwaltung, der Mengenänderungen, Preisberechnung und Rabattlogik kapselt. Diese Logik ist in einer E-Commerce-App auf Web und Mobile identisch, nur die Darstellung der Ergebnisse unterscheidet sich. Durch die Auslagerung in packages/shared-logic wird ein Bugfix oder eine neue Rabattregel nur an einer Stelle implementiert und automatisch auf beiden Plattformen wirksam, ohne Gefahr, dass Web und Mobile mit der Zeit auseinanderlaufen.


// packages/shared-logic/src/useCart.ts
// Platform-agnostic cart logic, shared between web and React Native
import { useState, useCallback, useMemo } from 'react';
import type { CartItem, Product } from '@myapp/shared-types';

export function useCart() {
  const [items, setItems] = useState<CartItem[]>([]);

  const addItem = useCallback((product: Product, quantity = 1) => {
    setItems((prev) => {
      const existing = prev.find((i) => i.productId === product.id);
      if (existing) {
        return prev.map((i) =>
          i.productId === product.id
            ? { ...i, quantity: i.quantity + quantity }
            : i
        );
      }
      return [...prev, { productId: product.id, quantity, price: product.price }];
    });
  }, []);

  const total = useMemo(
    () => items.reduce((sum, item) => sum + item.price * item.quantity, 0),
    [items]
  );

  return { items, addItem, total };
}

4. Geteilte Typen und API-Clients

TypeScript-Typen für API-Antworten, Domänenmodelle und Formularschemata sollten immer in einem gemeinsamen Package liegen, unabhängig davon, ob sonst viel oder wenig Code geteilt wird. Eine geteilte Product-Typdefinition verhindert, dass Web und React Native mit der Zeit inkompatible Annahmen über die Datenstruktur treffen, ein Problem, das sonst erst spät und schwer nachvollziehbar in der Produktion auffällt.

Der API-Client selbst, meist ein dünner Wrapper um fetch mit Fehlerbehandlung und Retry-Logik, lässt sich ebenfalls vollständig teilen, solange er keine plattformspezifische Authentifizierung enthält. Wichtig ist, den Zugriff auf sicheren Speicher, etwa AsyncStorage in React Native gegenüber localStorage im Web, über eine gemeinsame Schnittstelle zu abstrahieren, deren konkrete Implementierung pro Plattform ausgetauscht wird. So bleibt der API-Client selbst plattformunabhängig, während nur die darunterliegende Speicherimplementierung variiert.

5. Plattformspezifische Dateien für die UI-Schicht

Für die Präsentationsschicht, die naturgemäß nicht identisch geteilt werden kann, bietet der React-Native-Bundler Metro einen eleganten Mechanismus: Dateiendungen wie .native.tsx und .web.tsx. Eine Datei namens ProductCard.native.tsx wird automatisch nur für React Native aufgelöst, ProductCard.web.tsx nur für die Web-App, während beide über denselben Import-Pfad ./ProductCard angesprochen werden. Der Aufrufcode muss dadurch keine Verzweigungslogik enthalten.

Dieser Mechanismus funktioniert am saubersten, wenn beide Varianten dieselbe Props-Schnittstelle implementieren, definiert in einem geteilten Typ aus packages/shared-types. So bleibt sichergestellt, dass die Web- und die native Variante einer Komponente austauschbar bleiben, ohne dass Aufrufer wissen müssen, auf welcher Plattform sie gerade laufen. Bei Next.js-Projekten übernimmt ein zusätzliches Babel- oder Webpack-Setup dieselbe Auflösung, da Next.js standardmäßig nicht dieselbe Plattformdatei-Konvention wie Metro kennt.


// ProductCard.native.tsx - resolved automatically inside the Expo app
import { View, Text, Pressable } from 'react-native';
import type { ProductCardProps } from '@myapp/shared-types';

export function ProductCard({ product, onAddToCart }: ProductCardProps) {
  return (
    <Pressable onPress={() => onAddToCart(product)}>
      <View style={{ padding: 16 }}>
        <Text style={{ fontWeight: '600' }}>{product.name}</Text>
        <Text>{product.price.toFixed(2)} EUR</Text>
      </View>
    </Pressable>
  );
}

// ProductCard.web.tsx - resolved automatically inside the Vite/Next.js app
export function ProductCardWeb({ product, onAddToCart }: ProductCardProps) {
  return (
    <button onClick={() => onAddToCart(product)} className="p-4 text-left">
      <p className="font-semibold">{product.name}</p>
      <p>{product.price.toFixed(2)} EUR</p>
    </button>
  );
}

6. Styling teilen mit NativeWind

NativeWind übersetzt Tailwind-CSS-Klassennamen in React Native-Style-Objekte zur Build-Zeit und ist damit der praktikabelste Weg, ein einheitliches Design-System über Web und Mobile hinweg zu pflegen. Statt zwei völlig getrennte Styling-Ansätze zu warten, CSS für Web und StyleSheet-Objekte für Native, schreiben Teams mit NativeWind dieselben Utility-Klassen in beiden Umgebungen, etwa className="p-4 bg-slate-900 rounded-xl".

Design-Tokens wie Farben, Abstände und Typografie lassen sich in einer gemeinsamen Tailwind-Konfiguration in packages/design-tokens definieren und von beiden Apps referenzieren. Das verhindert Design-Drift zwischen Web und Mobile, ein Problem, das in Cross-Platform-Teams ohne geteiltes Token-System schleichend entsteht, weil kleine Anpassungen an Farbwerten in der einen App vorgenommen werden, ohne die andere zu aktualisieren.

7. React Native Web als Sonderfall

React Native Web verfolgt den umgekehrten Ansatz: Statt React-Web-Komponenten in React Native zu übernehmen, kompiliert diese Bibliothek React Native-Komponenten wie View und Text zu DOM-Elementen im Browser. Dieser Ansatz eignet sich besonders für Teams, die primär mobil denken und die Web-App als zusätzliche Ausgabeplattform derselben Codebasis behandeln wollen, etwa bei Expo-Projekten mit expo start --web.

Der Kompromiss dabei: Das resultierende Web-Markup ist oft weniger semantisch als handgeschriebenes HTML, und SEO-kritische Seiten profitieren meist mehr von einer echten Web-first-Lösung mit Next.js. React Native Web eignet sich hervorragend für interne Tools, Admin-Dashboards und Anwendungen ohne SEO-Anforderungen, ist aber selten die richtige Wahl für öffentliche, suchmaschinenoptimierte Marketing-Seiten.

8. Fallstricke bei geteiltem Code

Ein häufiger Fallstrick ist das versehentliche Einschleusen von plattformspezifischem Code in ein eigentlich geteiltes Package, etwa ein import von react-native-async-storage in einem Hook, der auch von der Web-App genutzt werden soll. Solche Abhängigkeiten brechen den Build der Web-App sofort und sind meist erst durch einen fehlgeschlagenen CI-Lauf sichtbar, nicht durch lokale Entwicklung, wenn nur an der mobilen App gearbeitet wird.

Ein zweiter Fallstrick betrifft übermäßigen Ehrgeiz beim Teilen: Der Versuch, auch die Präsentationsschicht mit generischen Wrapper-Komponenten zu vereinheitlichen, führt regelmäßig zu Abstraktionen, die weder für Web noch für React Native optimal sind. Die Faustregel lautet, Logik konsequent zu teilen und Präsentation konsequent plattformspezifisch zu halten, statt beides zu vermischen.

9. Strategien im direkten Vergleich

Die folgende Übersicht zeigt, welche Code-Sharing-Strategie für welchen Anwendungsfall zwischen React Web und React Native am besten geeignet ist.

Schicht Teilbarkeit Empfohlene Strategie Grenzfall
Business-Logik, Hooks Fast 100 Prozent Gemeinsames Package, kein Plattform-Code Speicherzugriff abstrahieren
Typen und Verträge 100 Prozent Gemeinsames Types-Package Keiner
Präsentationskomponenten Gering Plattformdateien .native/.web Gleiche Props-Schnittstelle
Styling Mittel NativeWind mit geteilten Tokens Nicht jede CSS-Feature verfügbar
Ganze Oberfläche Nur bei internen Tools React Native Web Schwächer bei SEO-Anforderungen

Diese Übersicht zeigt: Ein pauschales Ziel von hundert Prozent geteiltem Code ist unrealistisch und meist auch nicht sinnvoll. Der pragmatische Ansatz teilt konsequent, was teilbar ist, und akzeptiert plattformspezifische Präsentation als bewusste Designentscheidung, nicht als Kompromiss.

Mironsoft

Monorepo-Architektur für Web- und React-Native-Teams

Web und Mobile aus einer Codebasis pflegen?

Wir bauen euer Turborepo-Monorepo auf, trennen geteilte Business-Logik sauber von plattformspezifischer Präsentation und richten CI-Pipelines für Web und React Native gemeinsam ein.

Monorepo-Setup

Turborepo, pnpm Workspaces und geteilte Packages von Grund auf

Logik-Extraktion

Bestehende Business-Logik aus Web-Only-Code plattformunabhängig auslagern

Design-System

NativeWind-Setup mit geteilten Design-Tokens für Web und Mobile

10. Zusammenfassung

Erfolgreiches Code-Sharing zwischen React Web und React Native beruht auf einer klaren Schichtentrennung: Business-Logik, Typen und API-Clients wandern nahezu vollständig in geteilte Packages eines Monorepos, während die Präsentationsschicht bewusst plattformspezifisch bleibt, unterstützt durch Metros Dateiendungs-Konvention .native.tsx und .web.tsx. Turborepo und pnpm Workspaces liefern die technische Basis für schnelle, gecachte Builds über mehrere Apps hinweg.

Wer diese Trennung konsequent umsetzt, vermeidet die typischen Fallstricke von Cross-Platform-Projekten: plattformspezifische Abhängigkeiten in geteiltem Code und überambitionierte Präsentations-Abstraktionen. NativeWind und geteilte Design-Tokens schließen die Lücke beim Styling, während React Native Web für interne Tools ohne SEO-Anforderungen eine sinnvolle Ergänzung sein kann.

Code teilen zwischen React Web und React Native, das Wichtigste auf einen Blick

Monorepo

Turborepo mit pnpm Workspaces trennt Apps von geteilten Packages und cached Builds pro Package.

Geteilte Logik

Custom Hooks, Typen und API-Clients ohne DOM-Abhängigkeit lassen sich nahezu vollständig teilen.

Plattformdateien

.native.tsx und .web.tsx lösen automatisch die richtige UI-Variante auf, gleiche Props-Schnittstelle.

Styling

NativeWind mit geteilten Design-Tokens verhindert Design-Drift zwischen Web und Mobile.

11. FAQ: Code teilen zwischen React Web und React Native

1Wie viel Code lässt sich realistisch teilen?
Oft 60 bis 80 Prozent der Gesamtlogik, hauptsächlich Business-Logik, Typen und API-Clients.
2Kann ich Oberflächenkomponenten direkt teilen?
Nicht unverändert, da Web und Native unterschiedliche Basiselemente nutzen. Logik teilen, Präsentation trennen.
3Warum Turborepo statt einfachem Monorepo?
Cached Build-Ergebnisse pro Package und definiert Task-Abhängigkeiten, hält CI-Laufzeiten kontrollierbar.
4Wie funktionieren .native.tsx und .web.tsx?
Metro löst sie automatisch nach Zielplattform auf, ohne Verzweigungslogik im Aufrufcode.
5Was ist NativeWind?
Übersetzt Tailwind-Klassen in native Style-Objekte, ermöglicht geteilte Design-Tokens.
6Was ist React Native Web?
Kompiliert native Komponenten zu DOM-Elementen, gut für interne Tools ohne SEO-Bedarf.
7Wie vermeide ich Plattform-Code in geteilten Packages?
Speicherzugriffe hinter einer gemeinsamen Schnittstelle abstrahieren, pro Plattform austauschen.
8Sollte ich Typen teilen?
Ja, verhindert inkompatible Datenannahmen zwischen Web und Native über die Zeit.
9Häufigster Fehler bei zu viel Sharing?
Generische Wrapper-Komponenten für die Präsentation, meist schlechter als native Implementierungen.
10Brauche ich ein Monorepo?
Nicht zwingend, aber praktisch fast immer sinnvoll für schnellen Entwicklungszyklus.