Expo Router v5: Authentifizierung und Protected Routes in React Native

Lerne, wie du mit Expo Router v5 und Stack.Protected eine robuste Authentifizierung in React Native aufbaust – mit TypeScript, RBAC, sicherem Token-Handling und Session Reload.

Expo Router v5: Auth & Protected Routes Setup

Warum Protected Routes in React Native jetzt unverzichtbar sind

Hand aufs Herz: Jede mobile App, die mit Benutzerkonten arbeitet, steht vor derselben Herausforderung – wie schütze ich bestimmte Bereiche vor unauthentifizierten Zugriffen? Bisher war das in React Native ein ziemlich nerviger Tanz aus manuellen Redirects, bedingtem Rendering und verschachtelten Navigation Guards. Die fielen bei Deep Links oder Race Conditions gerne mal auseinander.

Mit Expo Router v5 ändert sich das grundlegend.

Das neue Stack.Protected-Primitive macht Authentifizierungslogik deklarativ, typsicher und – was mich persönlich am meisten freut – endlich zuverlässig. Statt Redirect-Logik über mehrere Dateien zu verstreuen, definierst du Zugriffsregeln direkt im Layout, und der Router kümmert sich um den Rest. In diesem Leitfaden bauen wir gemeinsam eine vollständige Authentifizierungsarchitektur mit Expo Router v5, TypeScript und den aktuellen Best Practices.

Was sich in Expo Router v5 geändert hat

Expo Router v5 (erstmals ausgeliefert mit Expo SDK 53, jetzt stabil in SDK 55) ist kein inkrementelles Update. Es ist ein echter Paradigmenwechsel. Hier die wichtigsten Neuerungen:

  • Stack.Protected und Tabs.Protected: Erstklassige Komponenten für client-seitige Zugriffskontrolle, die Guards direkt in Layout-Dateien ermöglichen
  • Anchor Routes: Ein neues Konzept, bei dem alle Routen in einem Stack relativ zu einer Anker-Route navigieren – entscheidend für korrekte Redirect-Logik
  • Session Reload: Mit location.reload() kann die gesamte Hermes-Engine neu geladen werden, um bei Logout sämtlichen In-Memory-State zu löschen
  • Vereinfachte Navigationsstrategien: Statt drei Push-Strategien gibt es jetzt nur noch zwei klare Optionen
  • React Server Functions: Server-seitiges Rendering und API-Routes für echte Full-Stack-Apps

Die alte Methode mit useRootNavigationState und manuellen router.replace()-Aufrufen in useEffect-Hooks? Geschichte. Wer noch den alten Ansatz nutzt, sollte jetzt migrieren – die neue API ist nicht nur einfacher, sondern auch deutlich robuster gegen Edge Cases wie Deep Links und Tab-Wechsel.

Voraussetzungen und Projektsetup

Für dieses Tutorial brauchst du:

  • Expo SDK 55 (oder mindestens SDK 53 für Protected Routes)
  • Node.js 20+
  • TypeScript (empfohlen, aber nicht zwingend)
  • expo-secure-store für sichere Token-Speicherung

Neues Projekt erstellen

Falls du noch kein Expo-Projekt hast, erstelle eines mit dem aktuellen Template:

npx create-expo-app@latest meine-auth-app --template default@sdk-55
cd meine-auth-app

Expo Router ist im Default-Template bereits enthalten. Falls du ein bestehendes Projekt aktualisierst:

npx expo install expo-router react-native-safe-area-context react-native-screens expo-linking expo-constants expo-status-bar expo-secure-store

Projektstruktur anlegen

Expo Router verwendet dateibasiertes Routing – jede Datei im app-Verzeichnis wird automatisch zu einer Route. Für unsere Authentifizierungsarchitektur sieht die Struktur so aus:

app/
├── _layout.tsx          # Root Layout mit SessionProvider
├── sign-in.tsx          # Login-Screen (öffentlich)
├── sign-up.tsx          # Registrierung (öffentlich)
└── (app)/
    ├── _layout.tsx      # Tab-Layout für authentifizierte Bereiche
    ├── index.tsx         # Home-Screen
    ├── profile.tsx       # Profil-Screen
    └── (admin)/
        ├── _layout.tsx   # Admin-Bereich Layout
        └── dashboard.tsx # Admin-Dashboard
lib/
├── auth-context.tsx     # AuthContext und SessionProvider
└── useSecureStorage.ts  # Hook für sichere Token-Speicherung

Der Ordner (app) mit Klammern ist eine sogenannte Route Group – sie erscheint nicht in der URL, dient aber dazu, Layouts und Guards zu gruppieren. Bei (admin) funktioniert das genauso, was verschachtelte Zugriffskontrolle ermöglicht.

Sichere Token-Speicherung mit expo-secure-store

Bevor wir den AuthContext bauen, brauchen wir einen Mechanismus für sichere Token-Speicherung. AsyncStorage ist dafür nicht geeignet, weil es Daten unverschlüsselt ablegt. Stattdessen nutzen wir expo-secure-store, das auf iOS die Keychain und auf Android den Keystore verwendet.

// lib/useSecureStorage.ts
import { useCallback, useEffect, useReducer } from 'react';
import * as SecureStore from 'expo-secure-store';
import { Platform } from 'react-native';

type StorageState = [boolean, string | null]; // [isLoading, value]

type StorageAction =
  | { type: 'RESTORE'; value: string | null }
  | { type: 'SET'; value: string | null };

function reducer(state: StorageState, action: StorageAction): StorageState {
  switch (action.type) {
    case 'RESTORE':
      return [false, action.value];
    case 'SET':
      return [false, action.value];
    default:
      return state;
  }
}

export function useSecureStorage(
  key: string
): [StorageState, (value: string | null) => void] {
  const [state, dispatch] = useReducer(reducer, [true, null]);

  useEffect(() => {
    async function restore() {
      let value: string | null = null;
      try {
        if (Platform.OS === 'web') {
          value = localStorage.getItem(key);
        } else {
          value = await SecureStore.getItemAsync(key);
        }
      } catch (e) {
        console.warn('Fehler beim Laden des gespeicherten Werts:', e);
      }
      dispatch({ type: 'RESTORE', value });
    }
    restore();
  }, [key]);

  const setValue = useCallback(
    async (value: string | null) => {
      try {
        if (Platform.OS === 'web') {
          if (value === null) {
            localStorage.removeItem(key);
          } else {
            localStorage.setItem(key, value);
          }
        } else {
          if (value === null) {
            await SecureStore.deleteItemAsync(key);
          } else {
            await SecureStore.setItemAsync(key, value);
          }
        }
      } catch (e) {
        console.warn('Fehler beim Speichern des Werts:', e);
      }
      dispatch({ type: 'SET', value });
    },
    [key]
  );

  return [state, setValue];
}

Dieser Hook abstrahiert die Plattformunterschiede elegant: Auf nativen Plattformen wird der Keychain/Keystore genutzt, im Web fällt er auf localStorage zurück. Der Ladezustand läuft über den Reducer, damit wir den Splash Screen korrekt steuern können.

AuthContext und SessionProvider erstellen

So, jetzt wird's spannend. Der AuthContext ist das Herzstück unserer Authentifizierungsarchitektur – er stellt Session-Daten und Authentifizierungsfunktionen für die gesamte App bereit.

// lib/auth-context.tsx
import {
  createContext,
  use,
  useCallback,
  type PropsWithChildren,
} from 'react';
import { useSecureStorage } from './useSecureStorage';

interface AuthContextType {
  signIn: (token: string) => void;
  signOut: () => void;
  session: string | null;
  isLoading: boolean;
}

const AuthContext = createContext<AuthContextType | null>(null);

export function SessionProvider({ children }: PropsWithChildren) {
  const [[isLoading, session], setSession] = useSecureStorage('auth-token');

  const signIn = useCallback(
    (token: string) => {
      setSession(token);
    },
    [setSession]
  );

  const signOut = useCallback(() => {
    setSession(null);
  }, [setSession]);

  return (
    <AuthContext.Provider
      value={{
        signIn,
        signOut,
        session,
        isLoading,
      }}
    >
      {children}
    </AuthContext.Provider>
  );
}

export function useSession(): AuthContextType {
  const value = use(AuthContext);
  if (!value) {
    throw new Error(
      'useSession muss innerhalb eines SessionProvider verwendet werden'
    );
  }
  return value;
}

Beachte die Verwendung von use() statt useContext() – das ist die neue React-19-API, die in Expo SDK 55 (React 19.2.0) vollständig unterstützt wird. In einer echten Produktionsanwendung würde signIn natürlich einen API-Call an deinen Backend-Server machen und das zurückgegebene Token speichern.

Stack.Protected implementieren – das Root Layout

Jetzt kommt der Teil, auf den wir hingearbeitet haben: Das Root Layout mit Stack.Protected, um Routen basierend auf dem Authentifizierungsstatus zu schützen.

// app/_layout.tsx
import { Stack } from 'expo-router';
import * as SplashScreen from 'expo-splash-screen';
import { useEffect } from 'react';
import { SessionProvider, useSession } from '@/lib/auth-context';

// Splash Screen aktiv halten, bis die Session geladen ist
SplashScreen.preventAutoHideAsync();

export default function RootLayout() {
  return (
    <SessionProvider>
      <RootNavigator />
    </SessionProvider>
  );
}

function RootNavigator() {
  const { session, isLoading } = useSession();

  useEffect(() => {
    if (!isLoading) {
      SplashScreen.hideAsync();
    }
  }, [isLoading]);

  if (isLoading) {
    return null; // Splash Screen wird angezeigt
  }

  return (
    <Stack screenOptions={{ headerShown: false }}>
      {/* Öffentliche Routen: nur sichtbar, wenn NICHT eingeloggt */}
      <Stack.Protected guard={!session}>
        <Stack.Screen name="sign-in" />
        <Stack.Screen name="sign-up" />
      </Stack.Protected>

      {/* Geschützte Routen: nur sichtbar, wenn eingeloggt */}
      <Stack.Protected guard={!!session}>
        <Stack.Screen name="(app)" />
      </Stack.Protected>
    </Stack>
  );
}

Wie funktioniert das?

Das Prinzip ist tatsächlich ziemlich elegant: Stack.Protected nimmt eine guard-Prop entgegen. Wenn guard={true}, sind die enthaltenen Screens zugänglich. Bei guard={false} werden sie gesperrt, und der Router leitet automatisch zur nächsten verfügbaren Route um – der sogenannten Anchor Route.

Konkret heißt das:

  • Ist der Benutzer nicht eingeloggt (session === null), sind sign-in und sign-up zugänglich, aber (app) ist gesperrt
  • Ist der Benutzer eingeloggt (session !== null), ist (app) zugänglich, aber sign-in und sign-up sind gesperrt
  • Versucht ein eingeloggter Benutzer, per Deep Link auf /sign-in zuzugreifen, wird er automatisch zur Anchor Route innerhalb von (app) umgeleitet

Der entscheidende Vorteil gegenüber der alten Redirect-Methode: Keine Race Conditions, kein Flackern zwischen Screens und keine Möglichkeit, den Schutz über Deep Links zu umgehen. Ehrlich gesagt hätte ich mir das schon vor zwei Jahren gewünscht.

Login- und Registrierungs-Screens erstellen

Jetzt bauen wir die öffentlichen Screens für Login und Registrierung. Hier ein vollständiger Login-Screen mit Fehlerbehandlung und Keyboard-Avoiding:

// app/sign-in.tsx
import { useState } from 'react';
import {
  View,
  Text,
  TextInput,
  TouchableOpacity,
  StyleSheet,
  ActivityIndicator,
  KeyboardAvoidingView,
  Platform,
} from 'react-native';
import { Link, router } from 'expo-router';
import { useSession } from '@/lib/auth-context';

export default function SignInScreen() {
  const { signIn } = useSession();
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [isSubmitting, setIsSubmitting] = useState(false);
  const [error, setError] = useState<string | null>(null);

  async function handleSignIn() {
    if (!email || !password) {
      setError('Bitte E-Mail und Passwort eingeben.');
      return;
    }

    setIsSubmitting(true);
    setError(null);

    try {
      // In Produktion: API-Call an deinen Auth-Server
      const response = await fetch('https://api.beispiel.de/auth/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ email, password }),
      });

      if (!response.ok) {
        throw new Error('Ungültige Anmeldedaten');
      }

      const { token } = await response.json();
      signIn(token);
      router.replace('/(app)');
    } catch (err) {
      setError(
        err instanceof Error
          ? err.message
          : 'Ein unerwarteter Fehler ist aufgetreten.'
      );
    } finally {
      setIsSubmitting(false);
    }
  }

  return (
    <KeyboardAvoidingView
      behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
      style={styles.container}
    >
      <View style={styles.form}>
        <Text style={styles.title}>Willkommen zurück</Text>
        <Text style={styles.subtitle}>
          Melde dich an, um fortzufahren
        </Text>

        {error && (
          <View style={styles.errorContainer}>
            <Text style={styles.errorText}>{error}</Text>
          </View>
        )}

        <TextInput
          style={styles.input}
          placeholder="E-Mail-Adresse"
          value={email}
          onChangeText={setEmail}
          autoCapitalize="none"
          keyboardType="email-address"
          textContentType="emailAddress"
          autoComplete="email"
        />

        <TextInput
          style={styles.input}
          placeholder="Passwort"
          value={password}
          onChangeText={setPassword}
          secureTextEntry
          textContentType="password"
          autoComplete="current-password"
        />

        <TouchableOpacity
          style={[styles.button, isSubmitting && styles.buttonDisabled]}
          onPress={handleSignIn}
          disabled={isSubmitting}
        >
          {isSubmitting ? (
            <ActivityIndicator color="#fff" />
          ) : (
            <Text style={styles.buttonText}>Anmelden</Text>
          )}
        </TouchableOpacity>

        <Link href="/sign-up" style={styles.link}>
          Noch kein Konto? Jetzt registrieren
        </Link>
      </View>
    </KeyboardAvoidingView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#0a0a1a',
    justifyContent: 'center',
  },
  form: {
    paddingHorizontal: 24,
    gap: 16,
  },
  title: {
    fontSize: 28,
    fontWeight: '700',
    color: '#ccd6f6',
    marginBottom: 4,
  },
  subtitle: {
    fontSize: 16,
    color: '#8892b0',
    marginBottom: 16,
  },
  errorContainer: {
    backgroundColor: '#ff4444' + '20',
    padding: 12,
    borderRadius: 8,
  },
  errorText: {
    color: '#ff6b6b',
    fontSize: 14,
  },
  input: {
    backgroundColor: '#1a1a2e',
    borderRadius: 12,
    padding: 16,
    fontSize: 16,
    color: '#ccd6f6',
    borderWidth: 1,
    borderColor: '#2a2a4a',
  },
  button: {
    backgroundColor: '#6366f1',
    borderRadius: 12,
    padding: 16,
    alignItems: 'center',
    marginTop: 8,
  },
  buttonDisabled: {
    opacity: 0.6,
  },
  buttonText: {
    color: '#fff',
    fontSize: 16,
    fontWeight: '600',
  },
  link: {
    color: '#6366f1',
    textAlign: 'center',
    fontSize: 14,
    marginTop: 8,
  },
});

Geschützte Bereiche mit Tabs aufbauen

Innerhalb des geschützten (app)-Bereichs verwenden wir eine Tab-Navigation. Auch hier kann Tabs.Protected zum Einsatz kommen, um bestimmte Tabs nur für bestimmte Benutzerrollen sichtbar zu machen.

// app/(app)/_layout.tsx
import { Tabs } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';

export default function AppLayout() {
  return (
    <Tabs
      screenOptions={{
        tabBarActiveTintColor: '#6366f1',
        tabBarInactiveTintColor: '#8892b0',
        tabBarStyle: {
          backgroundColor: '#0a0a1a',
          borderTopColor: '#1a1a2e',
        },
        headerStyle: {
          backgroundColor: '#0a0a1a',
        },
        headerTintColor: '#ccd6f6',
      }}
    >
      <Tabs.Screen
        name="index"
        options={{
          title: 'Start',
          tabBarIcon: ({ color, size }) => (
            <Ionicons name="home-outline" size={size} color={color} />
          ),
        }}
      />
      <Tabs.Screen
        name="profile"
        options={{
          title: 'Profil',
          tabBarIcon: ({ color, size }) => (
            <Ionicons name="person-outline" size={size} color={color} />
          ),
        }}
      />
    </Tabs>
  );
}

Rollenbasierte Zugriffskontrolle mit verschachtelten Guards

Hier wird's richtig mächtig. Einer der besten Aspekte von Stack.Protected ist die Möglichkeit, Guards zu verschachteln. Damit lässt sich eine komplette rollenbasierte Zugriffskontrolle (RBAC) aufbauen – ganz ohne externe Libraries.

Erweitern wir zunächst den AuthContext um Benutzerrollen:

// lib/auth-context.tsx (erweitert)
interface User {
  id: string;
  email: string;
  role: 'user' | 'admin' | 'moderator';
}

interface AuthContextType {
  signIn: (token: string) => void;
  signOut: () => void;
  session: string | null;
  user: User | null;
  isLoading: boolean;
}

Jetzt können wir im Root Layout verschachtelte Guards definieren:

// app/_layout.tsx (erweitert mit Rollen)
function RootNavigator() {
  const { session, user, isLoading } = useSession();

  useEffect(() => {
    if (!isLoading) {
      SplashScreen.hideAsync();
    }
  }, [isLoading]);

  if (isLoading) return null;

  const isLoggedIn = !!session;
  const isAdmin = user?.role === 'admin';

  return (
    <Stack screenOptions={{ headerShown: false }}>
      <Stack.Protected guard={!isLoggedIn}>
        <Stack.Screen name="sign-in" />
        <Stack.Screen name="sign-up" />
      </Stack.Protected>

      <Stack.Protected guard={isLoggedIn}>
        <Stack.Screen name="(app)" />

        {/* Verschachtelt: Nur Admins können auf den Admin-Bereich zugreifen */}
        <Stack.Protected guard={isAdmin}>
          <Stack.Screen name="(admin)" />
        </Stack.Protected>
      </Stack.Protected>
    </Stack>
  );
}

Diese Verschachtelung funktioniert hierarchisch: Ein Benutzer muss zuerst eingeloggt sein (äußerer Guard), dann Admin sein (innerer Guard), um den Admin-Bereich zu sehen. Versucht ein eingeloggter Nicht-Admin, auf /(admin)/dashboard zuzugreifen, wird er automatisch zur Anchor Route im (app)-Bereich umgeleitet. Kein manuelles Redirect-Handling nötig.

Anchor Routes und Navigationsstrategien verstehen

Anchor Routes sind ein neues Konzept in Expo Router v5, das eng mit Protected Routes zusammenarbeitet. Im Grunde ist eine Anchor Route die Basis-Route, zu der der Router zurückleitet, wenn ein geschützter Screen nicht zugänglich ist.

Anchor Route konfigurieren

// app/(app)/_layout.tsx
export const unstable_settings = {
  anchor: 'index', // Die Home-Route ist der Anker
};

Das heißt: Wenn ein Benutzer versucht, auf eine geschützte Route zuzugreifen, und der Guard false zurückgibt, landet er auf /(app)/index – nicht auf irgendeinem zufälligen Screen.

Wann braucht man Anchor Routes?

Anchor Routes sind besonders wichtig bei:

  • Deep Links: Wenn ein Benutzer einen Deep Link zu einer geschützten Route öffnet, muss der Router wissen, wohin er umleiten soll
  • Modals in verschachtelten Stacks: Ohne Anchor wird der Screen hinter einem Modal möglicherweise gelöscht, was zu einem leeren Navigationskontext führt
  • Tab-Navigationen: Tabs mit verschachtelten Stacks benötigen Anchors, um beim Tab-Wechsel den korrekten Initial-Screen anzuzeigen

Navigationsstrategien in v5

Expo Router v5 hat die Navigationsstrategien vereinfacht – statt drei gibt es nur noch zwei:

  1. Push (Standard): Jede Navigation pusht einen neuen Screen auf den Stack. Ideal für Content-getriebene Apps wie soziale Netzwerke, wo man immer tiefer navigiert
  2. Push or Pop: Wenn der Ziel-Screen bereits im Stack existiert, wird zurück zu ihm gesprungen statt ein Duplikat zu erstellen. Ideal für Apps mit festem Seitenrepertoire
import { router } from 'expo-router';

// Standard: neuen Screen pushen
router.push('/profile');

// Mit Anchor-Kontext navigieren
router.push('/profile', { withAnchor: true });

// Ersetzen statt pushen (kein Zurück-Button)
router.replace('/(app)');

Sicherer Logout mit Session Reload

Beim Logout reicht es nicht, einfach das Token zu löschen. In-Memory-State, gecachte API-Antworten und React-Kontext-Daten können sensible Informationen enthalten. Das wird gerne übersehen (ich hab den Fehler selbst schon gemacht). Expo Router v5 bietet dafür ein elegantes Feature: location.reload().

// Sicherer Logout mit vollständigem State-Reset
import { useSession } from '@/lib/auth-context';

function ProfileScreen() {
  const { signOut } = useSession();

  async function handleSecureLogout() {
    // 1. Token löschen
    signOut();

    // 2. Hermes-Engine komplett neu laden
    // Löscht ALLEN JavaScript/React-State
    if (typeof location !== 'undefined' && location.reload) {
      location.reload();
    }
  }

  return (
    <TouchableOpacity onPress={handleSecureLogout}>
      <Text>Abmelden</Text>
    </TouchableOpacity>
  );
}

Der Aufruf von location.reload() startet die Hermes-Engine neu und löscht sämtlichen Runtime-State – React-Context, Zustand-Stores, TanStack-Query-Cache, alles. Das ist die sicherste Methode, um bei einem Logout wirklich alle sensiblen Daten aus dem Speicher zu entfernen.

Wichtig: Da die gesamte Engine neu gestartet wird, sollte location.reload() nur für den Logout verwendet werden. Der Neustart dauert einen kurzen Moment, in dem der Splash Screen erneut angezeigt wird – aber das ist ein akzeptabler Trade-off für echte Sicherheit.

Häufige Fehler und Best Practices

Nach einigen Projekten mit Expo Router v5 haben sich ein paar wiederkehrende Fallstricke herauskristallisiert. Hier die wichtigsten:

1. Protected Routes sind nur client-seitig

Das kann man nicht oft genug betonen: Stack.Protected verhindert nur client-seitige Navigation. Protected Routes ersetzen keine server-seitige Authentifizierung. Dein Backend muss jeden API-Request unabhängig autorisieren – immer.

2. SplashScreen korrekt steuern

Ohne korrekte SplashScreen-Steuerung sieht der Benutzer beim App-Start kurz den Login-Screen aufblitzen, bevor er zur App weitergeleitet wird. Das klassische Flacker-Problem:

// Falsch: SplashScreen wird zu früh versteckt
export default function RootLayout() {
  return (
    <SessionProvider>
      <RootNavigator />  {/* Session noch nicht geladen! */}
    </SessionProvider>
  );
}

// Richtig: SplashScreen erst nach Session-Laden verstecken
function RootNavigator() {
  const { isLoading } = useSession();

  useEffect(() => {
    if (!isLoading) {
      SplashScreen.hideAsync();
    }
  }, [isLoading]);

  if (isLoading) return null; // SplashScreen bleibt sichtbar
  // ...
}

3. Tokens nicht in AsyncStorage speichern

AsyncStorage speichert Daten im Klartext. Für Authentifizierungs-Tokens ist das ein Sicherheitsrisiko. Nutze immer expo-secure-store auf nativen Plattformen.

4. Guards nicht in einzelnen Screens implementieren

Der ganze Sinn von Stack.Protected ist, die Autorisierungslogik zu zentralisieren. Wenn du trotzdem in einzelnen Screens if (!session) return redirect('/sign-in') schreibst, untergräbst du diesen Vorteil. Halte Guards in den Layout-Dateien – dafür sind sie da.

5. Deep Links testen

Teste deine Protected Routes unbedingt mit Deep Links, sowohl eingeloggt als auch ausgeloggt. Das wird erstaunlich oft vergessen:

# iOS Simulator
npx uri-scheme open "meineapp://profile" --ios

# Android Emulator
adb shell am start -a android.intent.action.VIEW -d "meineapp://profile"

Zusammenfassung

Expo Router v5 hat die Authentifizierung in React Native von einem notwendigen Übel zu einem erstklassigen Feature gemacht. Stack.Protected und Tabs.Protected bieten eine deklarative, typsichere und robuste Möglichkeit, Zugriffsregeln direkt in Layout-Dateien zu definieren.

In Kombination mit Anchor Routes, Session Reload und verschachtelten Guards deckt das Framework praktisch jeden Authentifizierungs-Use-Case ab – von der einfachen Login-Sperre bis zur komplexen rollenbasierten Zugriffskontrolle.

Das Wichtigste zum Mitnehmen: Protected Routes sind ein mächtiges Client-seitiges Feature, ersetzen aber niemals eine ordentliche server-seitige Autorisierung. Die Sicherheit deiner App hängt am Backend – der Router sorgt nur dafür, dass sich die App für den Benutzer richtig anfühlt.

Häufig gestellte Fragen

Was ist der Unterschied zwischen Stack.Protected und manuellen Redirects in Expo Router?

Stack.Protected ist die offizielle, deklarative Methode ab Expo Router v5, um Routen basierend auf Bedingungen zu schützen. Im Gegensatz zu manuellen Redirects mit router.replace() in useEffect-Hooks verhindert Stack.Protected das kurze Aufblitzen geschützter Screens, ist robust gegen Deep-Link-Umgehungen und zentralisiert die Authentifizierungslogik in Layout-Dateien. Die alte Redirect-Methode funktioniert weiterhin, wird aber für neue Projekte nicht mehr empfohlen.

Kann ich Expo Router Protected Routes auch ohne Expo verwenden?

Nein, Stack.Protected ist ein spezifisches Feature von Expo Router und setzt das Expo-Framework voraus. Wenn du reines React Navigation ohne Expo verwendest, musst du weiterhin auf manuelle Redirect-Logik oder Community-Lösungen zurückgreifen. Allerdings lässt sich Expo auch in bestehende React-Native-Projekte integrieren – ein npx expo install expo reicht als erster Schritt.

Sind Protected Routes in Expo Router sicher genug für Produktions-Apps?

Protected Routes bieten eine hervorragende Benutzererfahrung, sind aber rein client-seitig. Sie verhindern, dass Benutzer über die Navigation auf geschützte Screens gelangen, aber sie schützen nicht die dahinterliegenden Daten. Jeder API-Endpoint muss weiterhin unabhängig autorisiert werden. Stell dir Protected Routes wie ein Türschloss an deiner App-Oberfläche vor – der Tresor im Keller (dein Backend) braucht sein eigenes Schloss.

Wie funktioniert die Authentifizierung mit Tabs.Protected?

Tabs.Protected funktioniert identisch zu Stack.Protected, nur eben für Tab-Navigatoren. Du kannst einzelne Tabs hinter Guards verstecken – beispielsweise einen „Profil"-Tab nur für eingeloggte Benutzer oder einen „Admin"-Tab nur für Administratoren. Nicht zugängliche Tabs werden automatisch aus der Tab-Leiste entfernt und können weder per Tap noch per Deep Link erreicht werden.

Wie setze ich den gesamten App-State beim Logout zurück?

Expo Router v5 bietet die location.reload()-Methode, die die Hermes-JavaScript-Engine komplett neu startet. Dadurch werden sämtliche In-Memory-Daten gelöscht – React-Context, State-Management-Stores, gecachte Daten, einfach alles. Das ist die sicherste Methode für einen vollständigen Logout. Alternativ kannst du auch nur das Token löschen und die Guards erledigen die Umleitung, aber dann bleiben möglicherweise sensible Daten im Speicher.

Über den Autor Editorial Team

Our team of expert writers and editors.