Nach etwa vier Monaten in der Entwicklung eines React-Native-Projekts stößt jedes Team an dieselbe unsichtbare Wand: Plötzlich misst ein einfacher Bildschirm nicht mehr 80, sondern 400 Zeilen Code. Ein großer Teil davon besteht aus useEffect-Ketten, die API-Aufrufe koordinieren, während Push-Benachrichtigungen die App in Zustände versetzen, die niemand reproduzieren kann.
Saubere Architektur in React Native ist kein Thema von Ordnerhierarchien oder Schichten – sie entscheidet darüber, ob Ihr Team die Anwendung noch verstehen und warten kann, wenn asynchrone Abläufe, Navigation und native Module aufeinandertreffen.
Warum die Standardarchitektur scheitert
Die gängige Praxis in React Native folgt dem Prinzip „Alles landet dort, wo es zuerst gebraucht wird“. API-Aufrufe landen im Handler, der sie auslöst. Der Zustand wird im Bildschirm verwaltet, der ihn anzeigt. Native Module werden direkt aus Buttons aufgerufen. Diese Struktur funktioniert in den ersten Wochen – doch sie scheitert an folgenden Szenarien:
- Asynchrone Abläufe überdauern ihre Aufrufer. Ein Nutzer startet eine Anfrage, wechselt dann per Benachrichtigung auf einen anderen Bildschirm. Das ursprüngliche Promise löst sich auf und versucht, einen Setter zu aktualisieren, der längst nicht mehr existiert.
- Native Module vermischen sich mit der Oberfläche. Ein Bildschirm ruft direkt
NativeModules.Audio.start()auf. Mit iOS 17 ändern sich die Audio-Session-Semantiken – plötzlich funktionieren drei Bildschirme nicht mehr, obwohl nur einer betroffen sein sollte.
- Authentifizierungs-Rennen. Ein Token-Refresh läuft ab, während drei weitere Anfragen gleichzeitig aktiv sind. Zwei Anfragen werden wiederholt, eine meldet den Nutzer ab, eine leakt das alte Token.
- Drift zwischen ähnlichen Logiken. Die Funktion „Nachricht senden“ existiert in zwei Bildschirmen. Einer erhält eine neue Validierungsregel – der andere nicht.
Das typische Muster dahinter:
const handleSend = async () => {
const res = await api.post('/messages', input)
setMessages(prev => [...prev, res.data])
}Dieser Code ist nicht falsch – das Problem entsteht erst, wenn dieselbe Logik ein zweites Mal, minimal abgewandelt, in einem anderen Bildschirm implementiert wird.
Drei Schichten, eine klare Regel
Vergessen Sie Diagramme mit zig Schichten. Die minimale Struktur für saubere Architektur besteht aus drei klar getrennten Bereichen:
- Präsentation: Bildschirme, Komponenten, Hooks. Verantwortlich für das Rendern und die Orchestrierung der Oberfläche.
- Domäne: Use Cases und reine Geschäftslogik. Enthält keine Abhängigkeiten zu
react,fetchoderNativeModules.
- Daten: API-Clients, lokale Speicherung, Brücken zu nativen Modulen. Kennt die Außenwelt und deren Eigenheiten.
Die zentrale Regel lautet: Die Oberfläche kommuniziert ausschließlich mit der Domäne – niemals direkt mit der Datenebene.
Use Cases als klare Grenzen
Der Unterschied liegt im Delegieren statt im Implementieren. Statt:
const handleSend = async () => {
const res = await api.post('/messages', input)
setMessages(prev => [...prev, res.data])
}wird die Logik in einen Use Case ausgelagert:
class SendMessage {
constructor(private repo: MessageRepository) {}
async execute(input: SendMessageInput) {
// Validierung, Geschäftsregeln, Orchestrierung
return this.repo.send(input)
}
}Der Use Case SendMessage wird zur einzigen Stelle, an der definiert ist, wie eine Nachricht tatsächlich versendet wird. Zwei Bildschirme, die denselben Use Case aufrufen, können nicht auseinanderdriften – weil es nur eine Instanz davon gibt.
Die Datenebene übernimmt die konkrete Implementierung über ein Repository-Interface:
interface MessageRepository {
send(input: SendMessageInput): Promise<Message>
}Der Use Case hängt von diesem Interface ab, während die Implementierung in der Datenebene liegt. Die Oberfläche importiert weder das Repository noch den Use Case direkt – stattdessen importiert sie den Use Case, ruft execute() auf und kümmert sich nicht weiter.
Warum React Native besondere Fallstricke birgt
Viele Artikel zu sauberer Architektur ignorieren einen entscheidenden Punkt: Webanwendungen haben zwei Schichten – Oberfläche und API. React Native hingegen kombiniert Oberfläche, API, Navigations-Lebenszyklus, native Module, Hintergrund-/Vordergrund-Übergänge und plattformspezifische Unterbrechungen. Jede dieser Schichten verstärkt die Kosten von Schichtvermischungen.
- Asynchrone Abläufe überleben ihre Bildschirme. Eine Anfrage startet auf Bildschirm A und löst sich erst aus, wenn der Nutzer bereits auf Bildschirm C ist. Greift die Auflösung auf lokale Setter, Navigations-Referenzen oder Context zu, die nicht mehr existieren, entsteht ein Fehler, der nur bei schnellen Nutzerinteraktionen auftritt. Ein Use Case bietet eine zentrale Stelle, um Abbruchlogik, Idempotenz oder die Frage „Hört der Aufrufer noch zu?“ zu steuern – der Bildschirm muss davon nichts wissen.
- Native Module gehören nicht in Handler. Der direkte Aufruf von
NativeModules.Audio.start()in einem Button-Handler verknüpft die Oberfläche mit plattformspezifischem Verhalten. Plattformspezifisches Verhalten ändert sich häufig – zwischen iOS- und Android-Versionen, zwischen Simulator und echtem Gerät. Verpacken Sie das Modul stattdessen in ein Repository und exponieren Sie einen Use Case wieStartRecording. Die Oberfläche wird plattformunabhängig, während die plattformspezifische Logik einen zentralen Ort hat, an dem sie gewartet werden kann – besonders, wenn iOS plötzlich seine Semantiken ändert.
- Authentifizierungs-Rennen und Rehydrierung. Ein klassischer Bug in React Native: Ein Token-Refresh überlappt sich mit drei laufenden Anfragen. Wenn die Auth-Logik auf einen Axios-Interceptor, einen Context-Provider und einen Bildschirm verteilt ist, wird das Rennen zum unlösbaren Problem. Es gibt keine zentrale Stelle, die serialisiert werden kann. Ein
RefreshSession-Use Case, der die Warteschlange kontrolliert, macht das Problem handhabbar – wenn auch langweilig, aber machbar.
Tests werden ehrlich
Der größte praktische Vorteil sauberer Architektur liegt nicht in der Wiederverwendbarkeit, sondern darin, dass Tests plötzlich ohne Framework auskommen.
it('sends a message via the repo', async () => {
const repo = new FakeMessageRepo()
const useCase = new SendMessage(repo)
await useCase.execute({ body: 'hi' })
expect(repo.sent).toHaveLength(1)
})Kein Render-Baum. Kein react-test-renderer. Keine gemockten NativeModules. Kein Detox. Der Use Case läuft in reinem Node und beendet sich in Millisekunden. Der Wert der Architektur zeigt sich daran, was testbar wird – nicht daran, wie „sauber“ der Code aussieht.
Häufige Fallstricke und wie man sie vermeidet
Nicht jede Architektur ist sauber, nur weil sie drei Ordner hat. Drei typische Fehler:
- „Ich mache das nur dieses eine Mal“ ist keine Architektur. Ein direkter API-Aufruf „nur dieses eine Mal“ führt dazu, dass dieselbe Logik an drei Stellen schlecht implementiert wird. Entweder die Grenze wird strikt durchgesetzt – oder sie existiert nicht.
- Drei Schichten für eine Zwei-Bildschirm-App sind Verschwendung. Bei einer einfachen Login- und List-Anwendung brauchen Sie keine Use-Case-Ebene. Wenden Sie diese Struktur erst an, wenn die Komplexität es rechtfertigt – meist zwischen dem dritten echten Feature und dem zweiten Entwickler im Team.
- Ordner sind keine Grenzen. Ein
domain/-Verzeichnis nützt nichts, wenn ein Bildschirm trotzdemfetchdirekt aufruft. Die Ordnerstruktur dient nur der Dokumentation. ESLint-Regeln und Code-Reviews sind die eigentlichen Durchsetzer der Architektur.
Der größte Aufwand entsteht anfangs: Ein neues Feature berührt jetzt drei Dateien statt einer. Doch dieser Aufwand zahlt sich aus, sobald sich Nutzerverhalten, Plattformen oder Geschäftsregeln ändern. Saubere Architektur ist kein Ziel – sie ist die einzige Möglichkeit, in der sich ständig wandelnden Welt von React Native langfristig produktiv zu bleiben.
KI-Zusammenfassung
React Native'de temiz mimari, uygulamalarınızı daha iyi yönetmenize ve bakımını yapmanıza yardımcı olur