Expo Router: verschachtelte Layouts und Modals
AI generated
RN
native
React Native / Navigation
Expo Router: verschachtelte Layouts und Modals
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.

12 Min. Lesezeit Expo Router Layouts Modals

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.

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.

11. FAQ: Expo Router Layouts und Modals: Das Wichtigste auf einen Blick

1Was ist eine _layout.tsx-Datei in Expo Router genau?
Sie ist eine React-Komponente, die alle Routen innerhalb desselben Ordners und aller Unterordner umschließt und dabei einen Navigator wie Stack, Tabs oder Drawer definiert. Sie funktioniert wie ein Layout-Wrapper und folgt derselben Verschachtelungslogik wie React-Komponenten.
2Wie wird ein Screen als Modal dargestellt?
Über die Option presentation: 'modal' in den Screen-Optionen eines Stack-Navigators, entweder direkt am einzelnen Screen oder einheitlich in den screenOptions einer eigenen Route Group wie (modals). Die native Navigation rendert den Screen dann als von unten einfahrendes Overlay statt als horizontale Transition.
3Was ist der Unterschied zwischen presentation modal und formSheet?
Modal nimmt den gesamten Bildschirm ein, während formSheet ab iOS 16 nur einen Teil des Bildschirms belegt und den Hintergrund sichtbar lässt. Auf Android existiert kein natives Äquivalent zu formSheet, weshalb dort weiterhin die vollflächige Modal-Darstellung greift.
4Warum verschwindet die Tab-Bar nicht automatisch, wenn ein Modal geöffnet wird?
Die Tab-Bar verschwindet nur dann korrekt, wenn der Modal-Screen als Geschwister-Route der Tab-Group im Root-Stack liegt, nicht innerhalb der Tab-Group selbst. Liegt der Modal-Screen fälschlich innerhalb von app/(tabs), bleibt die Tab-Bar sichtbar, weil sie Teil desselben Tabs-Navigators ist.
5Erzeugen Ordner mit Klammern wie (tabs) ein eigenes URL-Segment?
Nein, Route Groups in Klammern dienen ausschließlich der Organisation im Dateisystem und werden beim Aufbau der finalen URL entfernt. Eine Route unter app/(tabs)/index.tsx ist unter / erreichbar, nicht unter /(tabs)/.
6Wie werden Header-Einstellungen zwischen verschachtelten Layouts vererbt?
screenOptions in einer übergeordneten _layout.tsx gelten als Standardwerte für alle darunterliegenden Screens und Layouts, können aber auf jeder tieferen Ebene über die options des einzelnen Screens gezielt überschrieben werden. Diese Vererbung funktioniert kaskadierend über beliebig viele Ebenen.
7Wo sollte ein Context Provider für geteilten Zustand platziert werden?
Genau auf der Layout-Ebene, die dem tatsächlichen Sichtbarkeitsbereich der Daten entspricht. Ein zu weit oben platzierter Provider verursacht unnötige Re-Renders, ein zu tief platzierter ist für Geschwister-Routen außerhalb dieses Layouts gar nicht erreichbar.
8Funktioniert Deep Linking auf eine Modal-Route ohne zusätzliche Konfiguration?
Ja, Expo Router leitet die Linking-Konfiguration automatisch aus der Ordnerstruktur ab und rekonstruiert beim Öffnen eines Links sowohl den zugrunde liegenden Tab-Screen als auch den davor liegenden Modal-Screen, ohne dass eine manuelle Linking-Tabelle gepflegt werden muss.
9Was verursacht doppelte Header in verschachtelten Layouts am häufigsten?
Wenn sowohl ein übergeordnetes als auch ein untergeordnetes Layout unabhängig voneinander headerShown: true setzen, rendern beide Stack-Ebenen ihre eigene Kopfzeile übereinander. Die Lösung ist, den Header konsequent nur auf genau einer Ebene zu aktivieren.
10Ab welcher Verschachtelungstiefe wird der Render-Overhead spürbar?
Bei etwa fünf oder mehr ineinander liegenden Stack-Ebenen wird der zusätzliche Overhead in der Praxis messbar. Eine bewusst flach gehaltene Struktur mit klar getrennten Route Groups statt tiefer Ordnerhierarchien ist dann meist die robustere Lösung.