Expo Router i React Native: Den komplette guide til filbaseret navigation (2026)

Lær Expo Router v7 at kende med denne danske guide. Dækker filbaseret routing, Stack og Tabs navigation, dynamiske ruter, TypeScript-typesikkerhed, deep linking, autentificering og SDK 55-funktioner som Zoom-overgange og Native Tabs.

Hvorfor Expo Router er fremtiden for navigation i React Native

Hvis du har bygget React Native-apps de seneste par år, kender du garanteret smerten ved at konfigurere navigation manuelt. Endeløse navigator-definitioner, typedeklarationer for route-parametre der bliver forældede, og en linking-konfiguration der nemt løber løbsk. Det er ikke sjovt.

Med Expo Router er alt det (heldigvis) fortid.

Expo Router er en filbaseret router til React Native og webapplikationer. Den genererer automatisk dine ruter baseret på filstrukturen i din app/-mappe — tænk Next.js, men for mobilapps. Med Expo SDK 55 og Expo Router v7, som blev lanceret i 2026, er dette nu den officielt anbefalede måde at håndtere navigation på. Og ærligt talt? Det er en gamechanger.

I denne guide dykker vi ned i alt fra grundlæggende filbaseret routing til avancerede mønstre som autentificering, deep linking, typesikre ruter og de helt nye iOS-funktioner som Zoom-overgange og Stack.Toolbar. Så lad os komme i gang.

Kom i gang med Expo Router

Installation og opsætning

Den nemmeste måde at starte et nyt projekt med Expo Router er at bruge create-expo-app. Den sætter alt op for dig automatisk:

npx create-expo-app@latest min-app

Vil du tilføje Expo Router til et eksisterende projekt? Så skal du installere de nødvendige pakker manuelt:

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

Derefter opdaterer du din package.json med det korrekte entry point:

{
  "main": "expo-router/entry"
}

Og i app.json skal du sørge for, at scheme er sat korrekt til deep linking:

{
  "expo": {
    "scheme": "min-app",
    "web": {
      "bundler": "metro"
    }
  }
}

Det er bogstaveligt talt det hele. Du er klar til at bygge.

Filbaseret routing: Kernekonceptet

Idéen bag Expo Router er simpel: hver fil i din app/-mappe bliver automatisk til en rute. Ingen manuel registrering, ingen navigator-konfiguration. Filstien er URL-stien.

Grundlæggende filstruktur

Her er et typisk eksempel på, hvordan en projektstruktur kan se ud:

app/
  _layout.tsx        → Root layout (wrapper for hele appen)
  index.tsx          → Matcher "/" (startsiden)
  about.tsx          → Matcher "/about"
  settings/
    _layout.tsx      → Layout for settings-gruppen
    index.tsx        → Matcher "/settings"
    profile.tsx      → Matcher "/settings/profile"
    notifications.tsx → Matcher "/settings/notifications"

Hver fil eksporterer en React-komponent som default export, og den komponent bliver automatisk en skærm i din app. Så simpelt er det faktisk:

// app/about.tsx
import { View, Text, StyleSheet } from 'react-native';

export default function AboutScreen() {
  return (
    <View style={styles.container}>
      <Text style={styles.title}>Om denne app</Text>
      <Text>Bygget med Expo Router v7 og SDK 55</Text>
    </View>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1, justifyContent: 'center', alignItems: 'center' },
  title: { fontSize: 24, fontWeight: 'bold', marginBottom: 12 },
});

Navigation mellem skærme

Expo Router giver dig to måder at navigere på: deklarativt med <Link>-komponenten og imperativt med router-objektet. Jeg bruger personligt begge dele afhængigt af situationen — Link til simple navigationer og router når navigationen afhænger af logik:

import { Link, router } from 'expo-router';
import { View, Text, Pressable } from 'react-native';

export default function HomeScreen() {
  return (
    <View>
      {/* Deklarativ navigation med Link */}
      <Link href="/about">
        <Text>Gå til Om-siden</Text>
      </Link>

      {/* Imperativ navigation med router */}
      <Pressable onPress={() => router.push('/settings')}>
        <Text>Åbn Indstillinger</Text>
      </Pressable>

      {/* Replace fjerner nuværende skærm fra stakken */}
      <Pressable onPress={() => router.replace('/dashboard')}>
        <Text>Gå til Dashboard</Text>
      </Pressable>
    </View>
  );
}

Layouts: Stack, Tabs og Drawer

Layouts er de specielle _layout.tsx-filer, der definerer, hvordan grupper af ruter forholder sig til hinanden. De er hjertet i Expo Routers navigationsstruktur — og det er her, det virkelig begynder at blive fedt.

Stack-navigation

Stack er den mest grundlæggende navigationstype. Skærme stables simpelthen oven på hinanden:

// app/_layout.tsx
import { Stack } from 'expo-router';

export default function RootLayout() {
  return (
    <Stack>
      <Stack.Screen
        name="index"
        options={{ title: 'Hjem' }}
      />
      <Stack.Screen
        name="about"
        options={{ title: 'Om' }}
      />
      <Stack.Screen
        name="settings"
        options={{ title: 'Indstillinger' }}
      />
    </Stack>
  );
}

Tab-navigation

Tabs er den klassiske navigation med en bund-bjælke, som næsten alle apps bruger. Her er et eksempel med ikoner:

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

export default function TabsLayout() {
  return (
    <Tabs screenOptions={{ tabBarActiveTintColor: '#007AFF' }}>
      <Tabs.Screen
        name="index"
        options={{
          title: 'Hjem',
          tabBarIcon: ({ color, size }) => (
            <Ionicons name="home" size={size} color={color} />
          ),
        }}
      />
      <Tabs.Screen
        name="search"
        options={{
          title: 'Søg',
          tabBarIcon: ({ color, size }) => (
            <Ionicons name="search" size={size} color={color} />
          ),
        }}
      />
      <Tabs.Screen
        name="profile"
        options={{
          title: 'Profil',
          tabBarIcon: ({ color, size }) => (
            <Ionicons name="person" size={size} color={color} />
          ),
        }}
      />
    </Tabs>
  );
}

Native Tabs — nyt i SDK 55

Her er noget, der virkelig fangede min opmærksomhed: med SDK 55 introduceres Native Tabs, som bruger platformens egne tab-komponenter. Altså ægte native — med Material Design 3 og dynamiske farver på Android, og native system tabs på iOS:

// app/(tabs)/_layout.tsx
import { NativeTabs } from 'expo-router/unstable-native-tabs';

export default function NativeTabsLayout() {
  return (
    <NativeTabs>
      <NativeTabs.Screen name="index" />
      <NativeTabs.Screen name="search" />
      <NativeTabs.Screen name="profile" />
    </NativeTabs>
  );
}

Native Tabs håndterer automatisk safe area insets på begge platforme, og på Android tilpasser farverne sig til brugerens wallpaper via Material Design 3. Ret imponerende, faktisk.

Dynamiske ruter og parametre

Dynamiske ruter bruger firkantede parenteser i filnavnet til at matche variable URL-segmenter. Det fungerer præcis, som du nok forventer:

app/
  user/
    [id].tsx         → Matcher "/user/123", "/user/abc" osv.
  product/
    [...slug].tsx    → Catch-all: matcher "/product/a/b/c"

Du får adgang til parametrene med useLocalSearchParams-hooken:

// app/user/[id].tsx
import { useLocalSearchParams } from 'expo-router';
import { View, Text, ActivityIndicator } from 'react-native';
import { useState, useEffect } from 'react';

interface User {
  id: string;
  name: string;
  email: string;
}

export default function UserScreen() {
  const { id } = useLocalSearchParams<{ id: string }>();
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetch(`https://api.example.com/users/${id}`)
      .then(res => res.json())
      .then(data => {
        setUser(data);
        setLoading(false);
      })
      .catch(() => setLoading(false));
  }, [id]);

  if (loading) return <ActivityIndicator size="large" />;
  if (!user) return <Text>Bruger ikke fundet</Text>;

  return (
    <View style={{ flex: 1, padding: 20 }}>
      <Text style={{ fontSize: 24, fontWeight: 'bold' }}>{user.name}</Text>
      <Text style={{ color: '#666' }}>{user.email}</Text>
    </View>
  );
}

Typesikre ruter med TypeScript

Okay, dette er ærligt talt en af mine yndlingsfunktioner i Expo Router. Den automatiske generering af TypeScript-typer baseret på din filstruktur. Når du starter udviklingsserveren, genererer Expo CLI typedefinitioner — og det betyder, at fejlstavede rutenavne og manglende parametre bliver fanget af compileren, ikke først ved runtime.

// TypeScript fanger fejlen allerede ved kompilering
import { Link } from 'expo-router';

// ✅ Korrekt — ruten eksisterer
<Link href="/user/123">Se bruger</Link>

// ❌ TypeScript-fejl — ruten findes ikke
<Link href="/bruger/123">Se bruger</Link>

For at aktivere dette behøver du bare have TypeScript sat op i dit projekt. Expo CLI genererer automatisk en expo-env.d.ts-fil med de nødvendige typer. Filerne tilføjes automatisk til .gitignore, så de ikke roder i din versionskontrol.

Du kan også type query-parametre manuelt med generics, hvis du har brug for det:

// Manuelt typede query-parametre
const { q, page } = useLocalSearchParams<{
  q: string;
  page?: string;
}>();

Deep linking: Hvert skærmbillede har en URL

En af de største fordele ved Expo Router er, at deep linking bare virker. Ud af boksen. Hver fil i din app/-mappe har automatisk en tilsvarende URL, som fungerer som deep link på iOS, Android og web. Ingen ekstra konfiguration nødvendig.

Sådan fungerer det

Fordi din app bruger filbaseret routing, kan brugere dele links til specifikke skærme, push-notifikationer kan navigere direkte til en bestemt rute, og indgående URL'er håndteres automatisk. Det er virkelig en af de ting, der gør Expo Router til et no-brainer for nye projekter.

Til produktionsapps anbefales Universal Links (iOS) og App Links (Android) i stedet for custom URL schemes. Det kræver domæne-verifikation, men giver en langt mere sikker og pålidelig oplevelse:

// app.json — konfiguration af Universal Links
{
  "expo": {
    "scheme": "min-app",
    "ios": {
      "associatedDomains": ["applinks:example.com"]
    },
    "android": {
      "intentFilters": [
        {
          "action": "VIEW",
          "autoVerify": true,
          "data": [
            {
              "scheme": "https",
              "host": "example.com",
              "pathPrefix": "/"
            }
          ],
          "category": ["BROWSABLE", "DEFAULT"]
        }
      ]
    }
  }
}

Tilpasning af indgående links

Har du brug for at omskrive en URL — for eksempel fra en gammel legacy-sti? Det kan du gøre i dit root layout med usePathname()-hooken, der lader dig reagere på URL-ændringer, mens appen er åben.

Autentificering med redirect-mønsteret

Autentificering er nok det mønster, folk spørger mest om. Og heldigvis håndterer Expo Router det rigtig elegant med en kombination af React Context og route groups. Lad mig vise dig, hvordan det fungerer trin for trin.

Trin 1: Opret en AuthContext

// ctx.tsx
import { useContext, createContext, useState, useEffect } from 'react';
import AsyncStorage from '@react-native-async-storage/async-storage';

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

const AuthContext = createContext<AuthContextType>({
  signIn: () => {},
  signOut: () => {},
  session: null,
  isLoading: true,
});

export function useSession() {
  return useContext(AuthContext);
}

export function SessionProvider({ children }: { children: React.ReactNode }) {
  const [session, setSession] = useState<string | null>(null);
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    AsyncStorage.getItem('session').then((value) => {
      setSession(value);
      setIsLoading(false);
    });
  }, []);

  const signIn = async (token: string) => {
    await AsyncStorage.setItem('session', token);
    setSession(token);
  };

  const signOut = async () => {
    await AsyncStorage.removeItem('session');
    setSession(null);
  };

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

Trin 2: Wrap din app med SessionProvider

// app/_layout.tsx
import { Slot } from 'expo-router';
import { SessionProvider } from '../ctx';

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

Trin 3: Beskyt ruter med redirect

// app/(app)/_layout.tsx
import { Redirect, Stack } from 'expo-router';
import { useSession } from '../../ctx';
import { ActivityIndicator, View } from 'react-native';

export default function AppLayout() {
  const { session, isLoading } = useSession();

  if (isLoading) {
    return (
      <View style={{ flex: 1, justifyContent: 'center' }}>
        <ActivityIndicator size="large" />
      </View>
    );
  }

  if (!session) {
    return <Redirect href="/sign-in" />;
  }

  return <Stack />;
}

Trin 4: Login-skærmen

// app/sign-in.tsx
import { router } from 'expo-router';
import { useSession } from '../ctx';
import { View, Text, TextInput, Pressable, StyleSheet } from 'react-native';
import { useState } from 'react';

export default function SignInScreen() {
  const { signIn } = useSession();
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');

  const handleSignIn = async () => {
    // Erstat med rigtig API-kald
    const response = await fetch('https://api.example.com/login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ email, password }),
    });
    const { token } = await response.json();
    signIn(token);
    router.replace('/(app)');
  };

  return (
    <View style={styles.container}>
      <Text style={styles.title}>Log ind</Text>
      <TextInput
        style={styles.input}
        placeholder="Email"
        value={email}
        onChangeText={setEmail}
        autoCapitalize="none"
      />
      <TextInput
        style={styles.input}
        placeholder="Adgangskode"
        value={password}
        onChangeText={setPassword}
        secureTextEntry
      />
      <Pressable style={styles.button} onPress={handleSignIn}>
        <Text style={styles.buttonText}>Log ind</Text>
      </Pressable>
    </View>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1, justifyContent: 'center', padding: 20 },
  title: { fontSize: 28, fontWeight: 'bold', marginBottom: 24 },
  input: {
    borderWidth: 1, borderColor: '#ddd', borderRadius: 8,
    padding: 12, marginBottom: 12, fontSize: 16,
  },
  button: {
    backgroundColor: '#007AFF', borderRadius: 8,
    padding: 14, alignItems: 'center',
  },
  buttonText: { color: '#fff', fontSize: 16, fontWeight: '600' },
});

Det smarte ved dette mønster er, at alle ruter altid er definerede. Hvis en bruger uden login forsøger at åbne et deep link til en beskyttet rute, bliver de automatisk redirectet til login-skærmen. Og efter login? De sendes videre til den rute, de oprindeligt ville hen til. Ret smart.

Stack.Protected — det moderne alternativ

Med Expo Router v7 kom der også en mere deklarativ måde at håndtere rutebeskyttelse på med Stack.Protected. Hvis du foretrækker en kortere syntax, er det her værd at kigge på:

// Deklarativ rutebeskyttelse med Stack.Protected
import { Stack } from 'expo-router';
import { useSession } from '../../ctx';

export default function AppLayout() {
  const { session } = useSession();

  return (
    <Stack>
      <Stack.Protected guard={!!session}>
        <Stack.Screen name="dashboard" />
        <Stack.Screen name="profile" />
      </Stack.Protected>
      <Stack.Screen name="sign-in" />
    </Stack>
  );
}

Nye funktioner i Expo Router v7

Apple Zoom-overgange

Okay, lad os snakke om noget visuelt imponerende. Apple Zoom-overgangen skaber en flydende animation mellem skærme ved at zoome fra et kildeelement til destinationsskærmen. Den bruger iOS 18+ native zoom-API, og det bedste? Ingen ekstra konfiguration:

import { Link } from 'expo-router';
import { View, Text, Image } from 'react-native';

export default function ProductList() {
  return (
    <View>
      {products.map(product => (
        <Link.AppleZoom key={product.id} href={`/product/${product.id}`}>
          <Image source={{ uri: product.thumbnail }} />
          <Text>{product.name}</Text>
        </Link.AppleZoom>
      ))}
    </View>
  );
}

Zoom-overgangen er aktiveret som standard i SDK 55 og giver en følelse af rumlig sammenhæng mellem ruter. For eksempel kan et miniaturebillede zoome ud til et fuldbredde-banner på næste skærm. Det ser fantastisk ud i praksis.

Stack.Toolbar

Den nye Stack.Toolbar-API giver adgang til native iOS-toolbars med indbyggede animationer og fuld Liquid Glass-effekt. Du kan placere knapper, menuer og tilpassede views i headeren eller bunden af skærmen:

import { Stack } from 'expo-router';
import { Text, Pressable } from 'react-native';

export default function DetailScreen() {
  return (
    <>
      <Stack.Toolbar placement="bottom">
        <Pressable onPress={() => console.log('Del')}>
          <Text>Del</Text>
        </Pressable>
        <Pressable onPress={() => console.log('Gem')}>
          <Text>Gem</Text>
        </Pressable>
      </Stack.Toolbar>
      {/* Skærmindhold */}
    </>
  );
}

Route Groups og modale skærme

Route groups (parenteser i mappenavnet) lader dig organisere ruter uden at påvirke URL-strukturen. Det er utrolig nyttigt til at holde styr på ting, når dit projekt vokser:

app/
  (auth)/
    sign-in.tsx      → Matcher "/sign-in"
    sign-up.tsx      → Matcher "/sign-up"
  (app)/
    _layout.tsx      → Beskyttet layout med auth-check
    (tabs)/
      _layout.tsx    → Tab-navigation
      index.tsx      → Matcher "/"
      search.tsx     → Matcher "/search"
      profile.tsx    → Matcher "/profile"
    settings.tsx     → Matcher "/settings"
  modal.tsx          → Kan præsenteres som modal

For at vise en skærm som modal tilføjer du bare præsentationsstilen i dit layout:

// app/_layout.tsx
import { Stack } from 'expo-router';

export default function RootLayout() {
  return (
    <Stack>
      <Stack.Screen name="(app)" options={{ headerShown: false }} />
      <Stack.Screen name="(auth)" options={{ headerShown: false }} />
      <Stack.Screen
        name="modal"
        options={{ presentation: 'modal' }}
      />
    </Stack>
  );
}

Ofte stillede spørgsmål

Erstatter Expo Router React Navigation?

Ikke helt, nej. Expo Router er faktisk bygget oven på React Navigation og automatiserer det meste af konfigurationen. Du kan stadig bruge React Navigations API direkte, hvis du har brug for det. Tænk på Expo Router som et produktivitetslag oven på React Navigation — du får filbaseret routing, automatisk deep linking og typesikkerhed, uden at miste adgangen til det underliggende bibliotek.

Kan jeg bruge Expo Router i et eksisterende projekt?

Ja! Men Expo Router kræver Expo CLI med Metro bundler. Hvis dit projekt allerede bruger Expo, kan du tilføje Expo Router ved at installere pakkerne og flytte dine skærme til app/-mappen. Det fede er, at migrering fra React Navigation kan ske trinvist — du konverterer bare en rute ad gangen.

Virker Expo Router med webapps?

Ja, og det er en af de ting, der gør det så tiltalende. Expo Router understøtter universel routing på Android, iOS og i browseren. Alle ruter har automatisk en URL, og på web kan den endda generere statiske sider for bedre SEO. Det er en ægte "skriv én gang, kør alle steder"-løsning.

Hvordan håndterer Expo Router performance med mange ruter?

Expo Router understøtter async routes (bundle splitting), der lazy-loader ruter i produktionsmode. Det betyder, at kun den aktuelle rute og dens afhængigheder indlæses — ikke hele appen på én gang. For store apps med mange skærme gør det en mærkbar forskel på den initielle indlæsningstid.

Hvad er forskellen mellem Native Tabs og almindelige Tabs?

Kort sagt: de almindelige Tabs bruger React Navigations JavaScript-baserede tab-bar, som er fuldt tilpasselig men ikke native. Native Tabs (tilgængelig som beta i SDK 55) bruger platformens egne tab-komponenter — UITabBarController på iOS og Material Design 3 tabs på Android. Du får en mere autentisk native oplevelse med dynamiske farver og automatisk safe area-håndtering, men til gengæld færre tilpasningsmuligheder. Det er en afvejning, men for de fleste apps er den native følelse det værd.

Om Forfatteren Editorial Team

Our team of expert writers and editors.