Wie gemeinsame UI-Elemente über mehrere Routen-Ebenen geteilt werden und Modals sauber ins dateibasierte Routing passen
Sobald eine Expo-Router-App über eine einzelne Tab-Leiste hinauswächst, stellt sich unweigerlich die Frage, wie Header, Tab-Bars und andere gemeinsame UI-Elemente über mehrere Routen-Ebenen hinweg konsistent bleiben, ohne dass jeder einzelne Screen dieselbe Konfiguration wiederholt. Verschachtelte _layout-Dateien lösen genau dieses Problem, indem sie wie React-Komponenten ineinander greifen und dabei jede Ordnerebene um einen eigenen Navigator erweitern können. Dieser Artikel zeigt, wie diese Verschachtelung in der Praxis funktioniert, wie Modal-Präsentation dateibasiert gesteuert wird und wie eine Tab-Navigation mit einem modalen Detail-Screen darüber konkret aufgebaut wird.
Inhaltsverzeichnis
- 1. Verschachtelte _layout-Dateien: das Grundprinzip von Expo Router
- 2. Gemeinsame UI-Elemente über mehrere Routen-Ebenen teilen
- 3. Modal-Präsentation über dateibasiertes Routing steuern
- 4. Praktisches Beispiel: Tab-Navigation mit modalem Detail-Screen
- 5. Route Groups: Ordnung im Dateisystem ohne Einfluss auf die URL
- 6. Layout-spezifische Header-Konfiguration weitergeben
- 7. Zustand zwischen verschachtelten Layout-Ebenen teilen
- 8. Deep Linking zu verschachtelten Modal-Routen
- 9. Typische Fallstricke bei verschachtelten Layouts
- 10. Zusammenfassung
- 11. FAQ
1. Verschachtelte _layout-Dateien: das Grundprinzip von Expo Router
Expo Router bildet die Navigationsstruktur einer App direkt auf die Ordnerstruktur im app-Verzeichnis ab, wobei jede Datei mit dem Namen _layout.tsx als Wrapper für alle Routen innerhalb desselben Ordners und aller Unterordner fungiert. Ein Layout ist dabei technisch nichts anderes als eine React-Komponente, die einen Navigator zurückgibt, etwa einen Stack, ein Tabs-Element oder einen Drawer, und über die Slot- beziehungsweise Screen-Komponenten steuert, wie die darunterliegenden Routen gerendert werden. Diese Struktur erlaubt es, Navigation deklarativ über die Dateisystemhierarchie zu beschreiben, statt sie zentral in einer einzigen Konfigurationsdatei zu pflegen.
Der entscheidende Unterschied zu einer flachen Navigationskonfiguration liegt in der Verschachtelung selbst: Jede tiefere Ordnerebene kann einen eigenen, zusätzlichen Navigator einführen, der innerhalb des übergeordneten Navigators lebt. Ein Root-Layout unter app/_layout.tsx umschließt die gesamte App und eignet sich für globale Provider und die oberste Stack-Ebene, während ein weiter unten liegendes app/(tabs)/_layout.tsx nur den Bereich innerhalb der Tab-Navigation betrifft. Diese Verschachtelung folgt exakt derselben Logik wie das Verschachteln von React-Komponenten, nur dass Expo Router die Zuordnung automatisch aus dem Dateisystem ableitet.
2. Gemeinsame UI-Elemente über mehrere Routen-Ebenen teilen
Der eigentliche Nutzen verschachtelter Layouts zeigt sich, sobald mehrere Screens dieselbe Kopfzeile, dieselbe Tab-Bar oder denselben Rahmen benötigen, ohne dass diese Konfiguration in jeder einzelnen Datei erneut geschrieben wird. Ein app/(tabs)/_layout.tsx definiert die Tab-Bar einmalig zentral, und jeder Screen innerhalb dieses Ordners erbt automatisch dieselbe Tab-Leiste am unteren Bildschirmrand, ohne selbst irgendeine Navigator-Logik zu enthalten. Ändert sich später das Design der Tab-Bar, genügt eine einzige Änderung in der Layout-Datei, statt zehn oder zwanzig Screens einzeln anzupassen.
Dasselbe Prinzip greift bei tiefer verschachtelten Bereichen: Ein app/(tabs)/profile/_layout.tsx kann innerhalb des Profil-Tabs einen eigenen Stack mit eigenem Header-Stil einführen, der sich vom Header der übrigen Tabs unterscheidet, etwa mit einer anderen Hintergrundfarbe oder zusätzlichen Header-Buttons. Dieser innere Stack bleibt dabei vollständig innerhalb der äußeren Tab-Navigation eingebettet, sodass die Tab-Bar am unteren Rand sichtbar bleibt, während sich im oberen Bereich mehrere Profil-Unterseiten wie Einstellungen oder Sicherheit durch Vor- und Zurücknavigation wechseln lassen.
3. Modal-Präsentation über dateibasiertes Routing steuern
Expo Router behandelt Modals nicht als eigenständiges Konzept, sondern als eine bestimmte Präsentationsart innerhalb eines Stack-Navigators, die über die presentation-Option eines Screens gesetzt wird. Wird ein Screen mit presentation: 'modal' konfiguriert, rendert die zugrunde liegende native Navigation ihn als Overlay, das von unten nach oben einfährt und typischerweise mit einer Wischgeste nach unten wieder geschlossen werden kann, statt der üblichen horizontalen Stack-Transition. Diese Konfiguration lässt sich entweder direkt in der Screen-Definition des Stacks setzen oder, sauberer strukturiert, über eine eigene Route Group wie (modals), in deren Layout-Datei presentation: 'modal' für alle enthaltenen Routen einheitlich vorgegeben wird.
Auf iOS ab Version 16 unterstützt dieselbe Konfiguration zusätzlich formSheet als Präsentationsart, bei der das Modal nur einen Teil des Bildschirms einnimmt und der Hintergrund weiterhin sichtbar bleibt, was sich für kurze, fokussierte Interaktionen wie eine Mengenauswahl oder einen Filter-Dialog eignet. Auf Android greift stattdessen weiterhin die vollflächige Modal-Darstellung, da formSheet dort kein natives Äquivalent besitzt, weshalb ein Team bei plattformübergreifenden Apps bewusst entscheiden sollte, ob dieser Unterschied für den jeweiligen Anwendungsfall akzeptabel ist.
// app/(modals)/_layout.tsx
import { Stack } from 'expo-router';
export default function ModalsLayout() {
return (
<Stack
screenOptions={{
presentation: 'modal',
headerShown: true,
gestureEnabled: true,
}}
>
<Stack.Screen name="filter" options={{ title: 'Filter' }} />
<Stack.Screen name="[productId]" options={{ title: 'Produktdetails' }} />
</Stack>
);
}
4. Praktisches Beispiel: Tab-Navigation mit modalem Detail-Screen
Ein realistisches Szenario kombiniert eine Tab-Navigation als Hauptstruktur mit einem Detail-Screen, der als Modal über den Tabs erscheint, etwa wenn aus einer Produktliste heraus ein einzelnes Produkt in einer fokussierten Ansicht geöffnet wird. Die Ordnerstruktur dafür trennt bewusst zwei Route Groups: app/(tabs) enthält die eigentliche Tab-Navigation mit Screens wie index, favorites und account, während app/product/[id].tsx außerhalb der Tabs auf der Root-Ebene liegt und im Root-Stack als Modal präsentiert wird. Dadurch bleibt die Tab-Bar beim Öffnen des Produkts unsichtbar, weil das Modal den gesamten Bildschirm einnimmt, genau wie es Nutzerinnen und Nutzer von nativen iOS- und Android-Apps erwarten.
Der Root-Layout in app/_layout.tsx definiert den äußersten Stack, der sowohl die Tab-Group als auch die einzelne Modal-Route als Geschwister-Screens enthält. Beim Navigieren zu /product/42 über router.push wird der Produkt-Screen dadurch nicht innerhalb der Tabs, sondern als eigenständiger Screen im Root-Stack geöffnet, was der Grund dafür ist, warum die Tab-Bar korrekt verschwindet, sobald das Modal aktiv ist, und beim Schließen exakt an derselben Stelle innerhalb der Tabs wieder erscheint, an der die Navigation begonnen hat.
// app/_layout.tsx
import { Stack } from 'expo-router';
export default function RootLayout() {
return (
<Stack>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen
name="product/[id]"
options={{ presentation: 'modal', title: 'Produktdetails' }}
/>
</Stack>
);
}
// Aufruf aus einem beliebigen Tab-Screen heraus
import { router } from 'expo-router';
function openProduct(id: string) {
router.push(`/product/${id}`);
}
5. Route Groups: Ordnung im Dateisystem ohne Einfluss auf die URL
Ordnernamen in Klammern wie (tabs) oder (modals) werden von Expo Router als Route Groups behandelt, die ausschließlich der strukturellen Organisation im Dateisystem dienen und dabei bewusst kein eigenes URL-Segment erzeugen. Eine Route unter app/(tabs)/index.tsx ist unter der URL / erreichbar, nicht unter /(tabs)/, weil die Klammern beim Aufbau der finalen Route entfernt werden. Dieses Verhalten erlaubt es, Routen thematisch oder nach Layout-Zugehörigkeit zu gruppieren, ohne dass sich dadurch versehentlich die öffentlich sichtbare URL-Struktur verändert, was insbesondere bei Deep-Linking-Konfigurationen und bestehenden Web-Links wichtig ist.
In der Praxis entstehen dadurch häufig mehrere parallele Route Groups auf derselben Verzeichnisebene, etwa (tabs) für die Hauptnavigation, (modals) für alle Overlay-Screens und (auth) für den nicht angemeldeten Zustand der App. Jede dieser Gruppen kann eine eigene _layout.tsx mit eigenem Navigator besitzen, wodurch sich unterschiedliche Navigationsmodelle innerhalb derselben App sauber voneinander trennen lassen, ohne dass eine zentrale Konfigurationsdatei alle Fälle gleichzeitig abbilden müsste.
6. Layout-spezifische Header-Konfiguration weitergeben
Jede _layout-Datei kann über screenOptions Standardwerte für alle enthaltenen Screens setzen, etwa eine bestimmte Header-Hintergrundfarbe oder eine einheitliche Tint-Farbe für Zurück-Buttons, während ein einzelner Screen diese Vorgaben über seine eigenen options gezielt überschreiben kann. Diese Vererbung funktioniert kaskadierend über beliebig viele Verschachtelungsebenen hinweg, wodurch sich globale Vorgaben im Root-Layout setzen und in tiefer liegenden Layouts punktuell anpassen lassen, ohne die globale Konfiguration jedes Mal zu duplizieren.
Für dynamische Titel, etwa den Namen eines Produkts im Header einer Detailseite, liest der jeweilige Screen den passenden Routenparameter über useLocalSearchParams aus und übergibt ihn an Stack.Screen mit einer options-Funktion, die abhängig vom geladenen Datensatz einen individuellen Titel zurückgibt. Dieser Mechanismus funktioniert unabhängig davon, auf welcher Layout-Ebene sich der Screen befindet, und macht dynamische Header-Titel zu einer lokalen Angelegenheit des jeweiligen Screens, statt globale Zustände dafür zu benötigen.
7. Zustand zwischen verschachtelten Layout-Ebenen teilen
Weil jedes Layout eine gewöhnliche React-Komponente ist, lässt sich ein Context Provider direkt in einer _layout-Datei platzieren und umschließt dadurch automatisch alle darunterliegenden Routen, unabhängig davon, wie tief diese verschachtelt sind. Ein CartProvider im Layout von app/(tabs)/_layout.tsx stellt beispielsweise den Warenkorbzustand allen Tab-Screens zur Verfügung, während ein im Root-Layout platzierter AuthProvider zusätzlich auch für Modal-Routen und Authentifizierungs-Screens außerhalb der Tabs sichtbar bleibt.
Wichtig ist dabei, den Provider bewusst auf der richtigen Ebene zu platzieren, weil ein zu weit oben platzierter Context unnötig viele Komponenten bei jeder Änderung neu rendern lässt, während ein zu tief platzierter Context von Geschwister-Routen außerhalb dieses Layouts gar nicht erreichbar ist. Als Faustregel gilt, den Provider genau auf der Ebene zu platzieren, die dem tatsächlichen Sichtbarkeitsbereich der Daten entspricht, also weder im globalen Root-Layout für rein lokale Tab-Zustände noch innerhalb eines einzelnen Screens für Daten, die mehrere Screens gemeinsam benötigen.
// app/(tabs)/_layout.tsx
import { Tabs } from 'expo-router';
import { CartProvider } from '../../context/CartContext';
export default function TabsLayout() {
return (
<CartProvider>
<Tabs screenOptions={{ headerShown: true }}>
<Tabs.Screen name="index" options={{ title: 'Start' }} />
<Tabs.Screen name="favorites" options={{ title: 'Favoriten' }} />
<Tabs.Screen name="account" options={{ title: 'Konto' }} />
</Tabs>
</CartProvider>
);
}
8. Deep Linking zu verschachtelten Modal-Routen
Weil Expo Router die Linking-Konfiguration automatisch aus der Ordnerstruktur ableitet, funktioniert ein Deep Link auf eine Modal-Route wie meineapp://product/42 ohne zusätzliche manuelle Konfiguration einer Linking-Tabelle, wie sie bei klassischem React Navigation nötig wäre. Beim Öffnen eines solchen Links rekonstruiert Expo Router automatisch den vollständigen Navigationszustand, sodass sowohl der zugrunde liegende Tab-Screen im Hintergrund als auch der modale Produkt-Screen davor korrekt aufgebaut werden, obwohl der Nutzer die App gerade erst über diesen einen Link geöffnet hat.
Diese automatische Rekonstruktion ist entscheidend für ein konsistentes Zurück-Verhalten: Schließt die Nutzerin das Modal nach dem Öffnen über einen Deep Link, landet sie nicht auf einem leeren Bildschirm, sondern exakt auf dem Tab-Screen, der als logischer Ausgangspunkt der Navigation gilt. Für Fälle, in denen kein sinnvoller Ausgangspunkt rekonstruierbar ist, etwa bei einem sehr tief verschachtelten Link ohne vorherigen Navigationsverlauf, sollte ein Fallback über initialRouteName oder eine explizite Startseite definiert werden, damit die App nie in einem navigatorisch unerreichbaren Zustand landet.
9. Typische Fallstricke bei verschachtelten Layouts
Der häufigste Fehler entsteht, wenn sowohl ein übergeordnetes als auch ein untergeordnetes Layout jeweils headerShown: true setzen, wodurch zwei Header übereinander gerendert werden, weil beide Stack-Ebenen unabhängig voneinander ihre eigene Kopfzeile anzeigen. Die Lösung besteht darin, den Header konsequent nur auf genau einer Ebene zu aktivieren und ihn auf allen anderen Ebenen explizit mit headerShown: false zu deaktivieren, statt sich auf einen impliziten Standardwert zu verlassen, der sich zwischen Expo-Router-Versionen ändern kann.
Ein zweiter, subtilerer Fehler entsteht durch eine unbeabsichtigt platzierte _layout.tsx in einem Ordner, der eigentlich nur zur Gruppierung dienen sollte, wodurch ungewollt eine zusätzliche Navigator-Ebene eingeführt wird und sich das Übergangsverhalten zwischen Screens plötzlich unerwartet ändert. Zusätzlich steigt bei sehr tiefer Verschachtelung, etwa fünf oder mehr ineinander liegenden Stack-Ebenen, der Render-Overhead spürbar, weshalb sich eine bewusst flach gehaltene Layout-Struktur mit klar getrennten Route Groups in der Praxis besser bewährt als eine tief verschachtelte Ordnerhierarchie.
| Layout-Datei | Navigator-Typ | Wirkungsbereich | Typischer Einsatz |
|---|---|---|---|
| app/_layout.tsx | Stack | Gesamte App | Root-Navigation, globale Provider |
| app/(tabs)/_layout.tsx | Tabs | Alle Tab-Screens | Bottom-Tab-Bar mit gemeinsamem Header |
| app/(modals)/_layout.tsx | Stack, presentation: modal | Modal-Routen | Detail-Screens als Overlay |
| app/(tabs)/profile/_layout.tsx | Stack | Profil-Unterseiten | Verschachtelte Navigation innerhalb eines Tabs |
| app/product/[id].tsx | Screen (Root-Stack) | Einzelner Produkt-Screen | Modal außerhalb der Tab-Navigation |
Mironsoft
React-Native-App-Entwicklung und Magento-Anbindung
Eine mobile App zum Magento-Shop, die wirklich rund läuft?
Wir entwickeln React-Native-Apps, die sauber an die Magento REST- oder GraphQL-API angebunden sind, von der ersten Codezeile bis zur Veröffentlichung im App Store und bei Google Play.
App-Konzeption
Architektur und Feature-Umfang einer Magento-angebundenen App gemeinsam planen.
Magento-API-Integration
Produktkatalog, Warenkorb und Checkout sauber an die Shop-API anbinden.
Store-Veröffentlichung
App Store- und Google-Play-Freigabeprozess ohne Stolperfallen begleiten.
10. Zusammenfassung
Expo Router Layouts und Modals: Das Wichtigste auf einen Blick
Prinzip
Jede _layout.tsx wrappt alle Routen im selben Ordner und darunter, verschachtelte Ordner fügen weitere Navigator-Ebenen hinzu.
Modals
presentation: modal steuert die Overlay-Darstellung, entweder direkt am Screen oder zentral über eine (modals)-Gruppe.
Route Groups
Klammern wie (tabs) organisieren Dateien, erzeugen aber kein eigenes URL-Segment.
Fallstricke
Doppelte Header und tiefe Verschachtelung sind die häufigsten Ursachen für unerwartetes Navigationsverhalten.