Expo Router v roce 2026: Kompletní průvodce souborovým směrováním v React Native

Kompletní průvodce Expo Routerem pro React Native — od základního nastavení přes dynamické cesty a typovou bezpečnost až po API routes a React Server Components.

Navigace je srdcem každé mobilní aplikace — bez ní prostě nic nefunguje. Uživatel se potřebuje dostat z bodu A do bodu B, a pokud mu to zkomplikujete, odinstaluje vám aplikaci rychleji, než stihnete říct „React Navigation". A právě o navigaci to dnes bude.

V ekosystému React Native jsme roky používali React Navigation s jeho imperativním přístupem. Fungovalo to, ale přiznejme si — konfigurace navigátorů byla občas dost úmorná. V roce 2026 tu ale máme Expo Router, který přináší souborové směrování známé z Next.js nebo Remixu přímo do React Native. A upřímně? Je to obrovský krok vpřed.

Expo Router totiž není jen tenký obal nad React Navigation. Je to plnohodnotný navigační framework, který generuje celou navigační strukturu automaticky z vašich souborů a složek. Takže žádné ruční definice navigátorů, žádné registrování obrazovek. Vytvoříte soubor v adresáři app/ a máte novou cestu. Tak jednoduché to je.

Proč záleží na směrování založeném na souborech?

Klasický přístup k navigaci v React Native vyžadoval explicitní definici každé obrazovky. Vytvářeli jste navigátory, vnořovali je do sebe a ručně spravovali celý strom. S rostoucí velikostí aplikace se to stávalo nepřehledným — a náchylným k chybám, což je přesně to, co nechcete.

Souborové směrování tohle elegantně řeší:

  • Konvence místo konfigurace — struktura složek přímo odpovídá navigační hierarchii
  • Automatický deep linking — každá obrazovka má svou URL, sdílení odkazů a testování je hračka
  • Typová bezpečnost — Expo Router generuje TypeScript typy pro všechny cesty
  • Univerzálnost — stejný kód běží na webu, iOS i Androidu
  • Snadná údržba — nová obrazovka = nový soubor, žádné konfigurační soubory

Nastavení Expo Routeru v novém projektu (Expo SDK 55)

Expo SDK 55, které vyšlo na začátku roku 2026, přineslo pár docela zásadních změn. Obsahuje React Native 0.83.1 a React 19.2, takže máte plnou podporu novinek jako Server Components. Za zmínku stojí i to, že SDK 55 kompletně zrušilo starší architekturu — všechno teď jede na Fabric a TurboModules.

Založit nový projekt je jednoduché:

# Vytvoření nového projektu s šablonou pro Expo Router
npx create-expo-app@latest moje-aplikace --template tabs

# Přechod do adresáře projektu
cd moje-aplikace

# Spuštění vývojového serveru
npx expo start

Chcete přidat Expo Router do existujícího projektu? Stačí nainstalovat potřebné balíčky:

# Instalace Expo Routeru a souvisejících balíčků
npx expo install expo-router expo-linking expo-constants expo-status-bar

A pak upravit app.json:

{
  "expo": {
    "name": "moje-aplikace",
    "slug": "moje-aplikace",
    "scheme": "mojeaplikace",
    "web": {
      "bundler": "metro",
      "output": "server"
    },
    "plugins": ["expo-router"],
    "experiments": {
      "typedRoutes": true
    }
  }
}

Ještě nastavte vstupní bod v package.json:

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

A to je vše. Expo Router bude automaticky skenovat adresář app/ a generovat navigaci podle nalezených souborů.

Základní koncepty: souborové směrování a adresářová struktura

Jádrem celého Expo Routeru je adresář app/ v kořenu projektu. Každý soubor, který exportuje React komponentu jako výchozí export, se automaticky stane navigační cestou. Název souboru = URL. Jasné a přehledné.

Základní adresářová struktura

app/
├── _layout.tsx          # Kořenový layout celé aplikace
├── index.tsx            # Domovská obrazovka (cesta: /)
├── about.tsx            # O aplikaci (cesta: /about)
├── settings.tsx         # Nastavení (cesta: /settings)
└── profile/
    ├── _layout.tsx      # Layout pro sekci profilu
    ├── index.tsx        # Profil (cesta: /profile)
    └── edit.tsx         # Úprava profilu (cesta: /profile/edit)

Vidíte, jak intuitivní to je? Soubor index.tsx v jakékoli složce představuje výchozí cestu té složky. Podsložky vytváří vnořené cesty. Žádná magie.

Soubory _layout.tsx

Soubory _layout.tsx definují, jak se budou vykreslovat podřízené obrazovky — jsou to vlastně ekvivalenty navigátorů z React Navigation. Kořenový layout je povinný a určuje hlavní navigační strukturu.

// app/_layout.tsx — Kořenový layout aplikace
import { Stack } from 'expo-router';

export default function KorenovyLayout() {
  return (
    <Stack
      screenOptions={{
        headerStyle: { backgroundColor: '#1a1a2e' },
        headerTintColor: '#ffffff',
        headerTitleStyle: { fontWeight: 'bold' },
      }}
    >
      <Stack.Screen
        name="index"
        options={{ title: 'Domů' }}
      />
      <Stack.Screen
        name="about"
        options={{ title: 'O aplikaci' }}
      />
    </Stack>
  );
}

A jednoduchá obrazovka? Ta vypadá jako úplně normální React komponenta:

// app/index.tsx — Domovská obrazovka
import { View, Text, StyleSheet } from 'react-native';
import { Link } from 'expo-router';

export default function DomovskaObrazovka() {
  return (
    <View style={styly.kontejner}>
      <Text style={styly.nadpis}>Vítejte v aplikaci</Text>
      <Link href="/about" style={styly.odkaz}>
        O aplikaci
      </Link>
      <Link href="/profile" style={styly.odkaz}>
        Můj profil
      </Link>
    </View>
  );
}

const styly = StyleSheet.create({
  kontejner: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    padding: 20,
  },
  nadpis: {
    fontSize: 28,
    fontWeight: 'bold',
    marginBottom: 20,
  },
  odkaz: {
    fontSize: 18,
    color: '#4a90d9',
    marginVertical: 8,
  },
});

Typy layoutů: Stack, Tabs a Drawer

Expo Router nabízí tři základní typy layoutů odpovídající navigátorům z React Navigation. Každý se hodí na něco jiného a můžete je libovolně kombinovat.

Stack (zásobníkový navigátor)

Stack je ta nejzákladnější forma navigace. Obrazovky se vrství na sebe a uživatel se vrací gestem přejetí nebo tlačítkem zpět. Použití je příjemně jednoduché.

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

export default function StackLayout() {
  return (
    <Stack>
      <Stack.Screen
        name="index"
        options={{
          title: 'Hlavní stránka',
          headerLargeTitle: true,
        }}
      />
      <Stack.Screen
        name="detail"
        options={{
          presentation: 'modal', // Zobrazení jako modální okno
        }}
      />
    </Stack>
  );
}

Tabs (záložková navigace)

Záložky jsou asi nejpoužívanějším typem navigace v mobilních appkách. Prakticky každá větší aplikace je má. Expo Router je řeší komponentou Tabs.

// app/(tabs)/_layout.tsx — Záložková navigace
import { Tabs } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';

export default function ZalozkovyLayout() {
  return (
    <Tabs
      screenOptions={{
        tabBarActiveTintColor: '#4a90d9',
        tabBarInactiveTintColor: '#888888',
        tabBarStyle: {
          backgroundColor: '#ffffff',
          borderTopWidth: 1,
          borderTopColor: '#e0e0e0',
        },
      }}
    >
      <Tabs.Screen
        name="index"
        options={{
          title: 'Domů',
          tabBarIcon: ({ color, size }) => (
            <Ionicons name="home" size={size} color={color} />
          ),
        }}
      />
      <Tabs.Screen
        name="search"
        options={{
          title: 'Hledat',
          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>
  );
}

Drawer (boční panel)

Drawer navigace — ten boční panel, který vysunete gestem z okraje obrazovky. Potřebujete k ní doinstalovat jeden balíček navíc:

# Instalace závislostí pro Drawer navigaci
npx expo install @react-navigation/drawer react-native-gesture-handler react-native-reanimated
// app/_layout.tsx — Drawer navigace
import { Drawer } from 'expo-router/drawer';
import { GestureHandlerRootView } from 'react-native-gesture-handler';

export default function DrawerLayout() {
  return (
    <GestureHandlerRootView style={{ flex: 1 }}>
      <Drawer
        screenOptions={{
          drawerStyle: {
            backgroundColor: '#1a1a2e',
            width: 280,
          },
          drawerLabelStyle: {
            color: '#ffffff',
          },
        }}
      >
        <Drawer.Screen
          name="index"
          options={{
            drawerLabel: 'Domovská stránka',
            title: 'Domů',
          }}
        />
        <Drawer.Screen
          name="settings"
          options={{
            drawerLabel: 'Nastavení',
            title: 'Nastavení aplikace',
          }}
        />
      </Drawer>
    </GestureHandlerRootView>
  );
}

Dynamické cesty a parametry

Dynamické cesty potřebujete všude tam, kde zobrazujete obsah podle nějakého identifikátoru — detail produktu, uživatelský profil, článek. V Expo Routeru definujete dynamické segmenty jednoduše hranatými závorkami v názvu souboru.

Základní dynamické cesty

app/
├── products/
│   ├── index.tsx          # Seznam produktů (cesta: /products)
│   └── [id].tsx           # Detail produktu (cesta: /products/123)
├── blog/
│   └── [slug].tsx         # Článek blogu (cesta: /blog/muj-clanek)
└── users/
    └── [userId]/
        ├── index.tsx      # Profil uživatele (cesta: /users/42)
        └── posts.tsx      # Příspěvky uživatele (cesta: /users/42/posts)

K parametrům se v komponentě dostanete přes hook useLocalSearchParams:

// app/products/[id].tsx — Detail produktu s dynamickou cestou
import { View, Text, StyleSheet, ActivityIndicator } from 'react-native';
import { useLocalSearchParams, Stack } from 'expo-router';
import { useState, useEffect } from 'react';

interface Produkt {
  id: string;
  nazev: string;
  popis: string;
  cena: number;
}

export default function DetailProduktu() {
  const { id } = useLocalSearchParams<{ id: string }>();
  const [produkt, setProdukt] = useState<Produkt | null>(null);
  const [nacitani, setNacitani] = useState(true);

  useEffect(() => {
    async function nacistProdukt() {
      try {
        const odpoved = await fetch(`https://api.example.com/products/${id}`);
        const data = await odpoved.json();
        setProdukt(data);
      } catch (chyba) {
        console.error('Chyba při načítání produktu:', chyba);
      } finally {
        setNacitani(false);
      }
    }
    nacistProdukt();
  }, [id]);

  if (nacitani) {
    return (
      <View style={styly.stred}>
        <ActivityIndicator size="large" color="#4a90d9" />
      </View>
    );
  }

  if (!produkt) {
    return (
      <View style={styly.stred}>
        <Text>Produkt nebyl nalezen.</Text>
      </View>
    );
  }

  return (
    <>
      <Stack.Screen options={{ title: produkt.nazev }} />
      <View style={styly.kontejner}>
        <Text style={styly.nazev}>{produkt.nazev}</Text>
        <Text style={styly.cena}>{produkt.cena} Kč</Text>
        <Text style={styly.popis}>{produkt.popis}</Text>
      </View>
    </>
  );
}

const styly = StyleSheet.create({
  kontejner: { flex: 1, padding: 20 },
  stred: { flex: 1, justifyContent: 'center', alignItems: 'center' },
  nazev: { fontSize: 24, fontWeight: 'bold', marginBottom: 8 },
  cena: { fontSize: 20, color: '#4a90d9', marginBottom: 16 },
  popis: { fontSize: 16, lineHeight: 24, color: '#444' },
});

Záchytné cesty (Catch-all routes)

Když potřebujete zachytit víc segmentů cesty najednou, hodí se syntaxe [...slug]. Typický případ použití? CMS s hlubokým vnořením kategorií.

// app/docs/[...slug].tsx — Záchytná cesta pro dokumentaci
import { useLocalSearchParams } from 'expo-router';
import { View, Text } from 'react-native';

export default function StrankaDocumentace() {
  const { slug } = useLocalSearchParams<{ slug: string[] }>();
  const cestaKDokumentaci = Array.isArray(slug) ? slug.join('/') : slug;

  return (
    <View style={{ flex: 1, padding: 20 }}>
      <Text style={{ fontSize: 18 }}>
        Dokumentace: {cestaKDokumentaci}
      </Text>
    </View>
  );
}

Skupiny cest a sdílené layouty

Tohle je jedna z věcí, které na Expo Routeru miluju. Složky s názvem v kulatých závorkách (třeba (auth) nebo (tabs)) vytvářejí logické skupiny, které se vůbec neprojeví v URL. Můžete si tak organizovat kód do smysluplných celků, aniž byste ovlivnili navigační strukturu.

Příklad organizace pomocí skupin

app/
├── _layout.tsx              # Kořenový layout
├── (auth)/                  # Skupina pro autentizaci (neprojeví se v URL)
│   ├── _layout.tsx          # Layout pro auth obrazovky
│   ├── login.tsx            # Přihlášení (cesta: /login)
│   └── register.tsx         # Registrace (cesta: /register)
├── (tabs)/                  # Skupina se záložkami (neprojeví se v URL)
│   ├── _layout.tsx          # Záložkový layout
│   ├── index.tsx            # Domů (cesta: /)
│   ├── explore.tsx          # Prozkoumat (cesta: /explore)
│   └── profile.tsx          # Profil (cesta: /profile)
└── (modals)/                # Skupina pro modální okna
    ├── _layout.tsx          # Layout pro modály
    └── settings.tsx         # Nastavení (cesta: /settings)

Kořenový layout pak může tyto skupiny seřadit do navigační hierarchie:

// app/_layout.tsx — Kořenový layout se skupinami
import { Stack } from 'expo-router';

export default function KorenovyLayout() {
  return (
    <Stack screenOptions={{ headerShown: false }}>
      <Stack.Screen name="(tabs)" />
      <Stack.Screen
        name="(auth)"
        options={{
          presentation: 'modal',
          animation: 'slide_from_bottom',
        }}
      />
      <Stack.Screen
        name="(modals)"
        options={{
          presentation: 'transparentModal',
          animation: 'fade',
        }}
      />
    </Stack>
  );
}

Typované cesty pro TypeScript

Tady se to začíná dělat opravdu zajímavé. Když v konfiguraci zapnete experiments.typedRoutes, Expo automaticky generuje TypeScript typy pro všechny cesty. To znamená automatické doplňování v editoru a okamžité upozornění, pokud se pokusíte navigovat na cestu, která neexistuje.

// Nastavení v app.json pro aktivaci typovaných cest
{
  "expo": {
    "experiments": {
      "typedRoutes": true
    }
  }
}

Po startu vývojového serveru Expo vygeneruje soubor s typy. Pak je využijete takhle:

// Příklad využití typovaných cest v komponentě
import { Link, router } from 'expo-router';
import { Pressable, Text, View } from 'react-native';

export default function NavigacniKomponenta() {
  function prejitNaDetail(idProduktu: string) {
    // TypeScript ověří, že cesta existuje a parametr je správný
    router.push({
      pathname: '/products/[id]',
      params: { id: idProduktu },
    });
  }

  function prejitNaHlavniStranku() {
    router.replace('/');
  }

  return (
    <View style={{ flex: 1, padding: 20 }}>
      <Link href="/profile" asChild>
        <Pressable style={{ padding: 12, backgroundColor: '#4a90d9', borderRadius: 8 }}>
          <Text style={{ color: '#fff', textAlign: 'center' }}>
            Přejít na profil
          </Text>
        </Pressable>
      </Link>

      <Link
        href={{
          pathname: '/products/[id]',
          params: { id: '42' },
        }}
      >
        Detail produktu č. 42
      </Link>

      <Pressable onPress={() => prejitNaDetail('99')}>
        <Text>Zobrazit produkt 99</Text>
      </Pressable>
    </View>
  );
}

Typované cesty vám ušetří spoustu bolesti hlavy. Když přejmenujete soubor v app/, TypeScript vás okamžitě upozorní na všechna místa, kde je třeba aktualizovat odkazy. U velkých projektů s desítkami obrazovek je to k nezaplacení.

Deep linking a univerzální odkazy

Jednou z věcí, kde Expo Router exceluje, je automatický deep linking. Každá obrazovka má svou URL — můžete ji otevřít z externího odkazu, push notifikace nebo z jiné aplikace. A nemusíte pro to nic zvlášť konfigurovat (kromě schématu).

Konfigurace schématu

Deep linking potřebuje definované schéma v app.json:

{
  "expo": {
    "scheme": "mojeaplikace",
    "ios": {
      "associatedDomains": ["applinks:www.mojedomena.cz"]
    },
    "android": {
      "intentFilters": [
        {
          "action": "VIEW",
          "autoVerify": true,
          "data": [
            {
              "scheme": "https",
              "host": "www.mojedomena.cz",
              "pathPrefix": "/"
            }
          ],
          "category": ["BROWSABLE", "DEFAULT"]
        }
      ]
    }
  }
}

S touto konfigurací vám budou fungovat následující scénáře:

  • mojeaplikace://products/42 — otevře detail produktu přes vlastní schéma
  • https://www.mojedomena.cz/products/42 — stejná obrazovka přes univerzální odkaz
  • Push notifikace s odkazem automaticky navigují na správnou obrazovku

Zpracování příchozích odkazů

// app/_layout.tsx — Zpracování deep linků v kořenovém layoutu
import { Stack } from 'expo-router';
import { useEffect } from 'react';
import * as Linking from 'expo-linking';

export default function KorenovyLayout() {
  useEffect(() => {
    const subscription = Linking.addEventListener('url', (udalost) => {
      console.log('Přijat deep link:', udalost.url);
    });
    return () => subscription.remove();
  }, []);

  return (
    <Stack>
      <Stack.Screen name="(tabs)" options={{ headerShown: false }} />
      <Stack.Screen name="products/[id]" options={{ title: 'Detail produktu' }} />
    </Stack>
  );
}

Na webu směrování funguje přes standardní URL automaticky, takže SEO a sdílení odkazů máte zadarmo. Oproti starému přístupu, kde jste museli deep linking konfigurovat ručně pro každou obrazovku, je to obrovský rozdíl.

Autentizace a chráněné cesty (Stack.Protected)

Řízení přístupu k chráněným částem aplikace — to je něco, co řeší snad každý projekt. Expo Router přichází s Stack.Protected, které umožňuje řízení přístupu na základě rolí přímo v navigaci. Je to čisté a přímočaré řešení.

Kontext pro autentizaci

// contexts/AuthContext.tsx — Kontext pro autentizaci
import { createContext, useContext, useState, useEffect, ReactNode } from 'react';

interface Uzivatel {
  id: string;
  jmeno: string;
  email: string;
  role: 'uzivatel' | 'admin' | 'editor';
}

interface AuthKontext {
  uzivatel: Uzivatel | null;
  prihlasit: (email: string, heslo: string) => Promise<void>;
  odhlasit: () => Promise<void>;
  nacitani: boolean;
}

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

export function AuthProvider({ children }: { children: ReactNode }) {
  const [uzivatel, setUzivatel] = useState<Uzivatel | null>(null);
  const [nacitani, setNacitani] = useState(true);

  useEffect(() => {
    kontrolovatRelaci();
  }, []);

  async function kontrolovatRelaci() {
    try {
      const odpoved = await fetch('/api/auth/session');
      if (odpoved.ok) {
        const data = await odpoved.json();
        setUzivatel(data.uzivatel);
      }
    } catch (chyba) {
      console.error('Chyba při kontrole relace:', chyba);
    } finally {
      setNacitani(false);
    }
  }

  async function prihlasit(email: string, heslo: string) {
    const odpoved = await fetch('/api/auth/login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ email, heslo }),
    });
    if (!odpoved.ok) throw new Error('Přihlášení se nezdařilo');
    const data = await odpoved.json();
    setUzivatel(data.uzivatel);
  }

  async function odhlasit() {
    await fetch('/api/auth/logout', { method: 'POST' });
    setUzivatel(null);
  }

  return (
    <AuthContext.Provider value={{ uzivatel, prihlasit, odhlasit, nacitani }}>
      {children}
    </AuthContext.Provider>
  );
}

export function useAuth() {
  const kontext = useContext(AuthContext);
  if (!kontext) throw new Error('useAuth musí být použit uvnitř AuthProvider');
  return kontext;
}

Použití Stack.Protected pro řízení přístupu

// app/_layout.tsx — Ochrana cest pomocí Stack.Protected
import { Stack } from 'expo-router';
import { AuthProvider, useAuth } from '../contexts/AuthContext';
import { View, ActivityIndicator } from 'react-native';

function ChranenyLayout() {
  const { uzivatel, nacitani } = useAuth();

  if (nacitani) {
    return (
      <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
        <ActivityIndicator size="large" color="#4a90d9" />
      </View>
    );
  }

  return (
    <Stack screenOptions={{ headerShown: false }}>
      <Stack.Screen name="(auth)" />
      <Stack.Protected guard={!!uzivatel}>
        <Stack.Screen name="(tabs)" />
        <Stack.Screen name="(modals)" options={{ presentation: 'modal' }} />
      </Stack.Protected>
      <Stack.Protected guard={uzivatel?.role === 'admin'}>
        <Stack.Screen name="admin" />
      </Stack.Protected>
    </Stack>
  );
}

export default function KorenovyLayout() {
  return (
    <AuthProvider>
      <ChranenyLayout />
    </AuthProvider>
  );
}

Stack.Protected přijímá vlastnost guard — boolean hodnotu. Když je false, všechny obrazovky uvnitř jsou nepřístupné a pokus o navigaci se automaticky přesměruje. Díky tomu implementujete řízení přístupu podle rolí docela snadno — běžní uživatelé vidí standardní obrazovky, admini mají navíc přístup k admin sekci.

API Routes (+api.ts)

Expo Router ale není jen o klientské navigaci. Podporuje taky serverové API cesty, takže si můžete vytvářet backendové endpointy přímo v projektu. Stačí přidat příponu +api.ts k souboru a běží na serveru.

Základní API cesta

// app/api/products+api.ts — API cesta pro seznam produktů
import { ExpoRequest, ExpoResponse } from 'expo-router/server';

const produkty = [
  { id: '1', nazev: 'React Native Průvodce', cena: 599 },
  { id: '2', nazev: 'TypeScript Masterclass', cena: 799 },
  { id: '3', nazev: 'Expo Workshop', cena: 499 },
];

// GET /api/products — Získání seznamu produktů
export function GET(request: ExpoRequest) {
  const url = new URL(request.url);
  const hledanyVyraz = url.searchParams.get('q');

  let vysledky = produkty;
  if (hledanyVyraz) {
    vysledky = produkty.filter((p) =>
      p.nazev.toLowerCase().includes(hledanyVyraz.toLowerCase())
    );
  }

  return ExpoResponse.json({
    data: vysledky,
    celkem: vysledky.length,
  });
}

// POST /api/products — Vytvoření nového produktu
export async function POST(request: ExpoRequest) {
  try {
    const telo = await request.json();
    if (!telo.nazev || !telo.cena) {
      return ExpoResponse.json(
        { chyba: 'Název a cena jsou povinné' },
        { status: 400 }
      );
    }

    const novyProdukt = {
      id: String(produkty.length + 1),
      nazev: telo.nazev,
      cena: telo.cena,
    };
    produkty.push(novyProdukt);

    return ExpoResponse.json(
      { data: novyProdukt },
      { status: 201 }
    );
  } catch (chyba) {
    return ExpoResponse.json(
      { chyba: 'Neplatná data požadavku' },
      { status: 400 }
    );
  }
}

Dynamické API cesty

// app/api/products/[id]+api.ts — API cesta pro konkrétní produkt
import { ExpoRequest, ExpoResponse } from 'expo-router/server';

export function GET(request: ExpoRequest, { id }: { id: string }) {
  const produkt = najitProduktPodleId(id);

  if (!produkt) {
    return ExpoResponse.json(
      { chyba: 'Produkt nebyl nalezen' },
      { status: 404 }
    );
  }

  return ExpoResponse.json({ data: produkt });
}

export function DELETE(request: ExpoRequest, { id }: { id: string }) {
  const smazan = smazatProdukt(id);

  if (!smazan) {
    return ExpoResponse.json(
      { chyba: 'Produkt nebyl nalezen' },
      { status: 404 }
    );
  }

  return ExpoResponse.json(
    { zprava: 'Produkt byl úspěšně smazán' },
    { status: 200 }
  );
}

API cesty se hodí na operace, které potřebují přístup k tajným klíčům, databázím nebo externím službám. Vše běží výhradně na serveru, takže citlivá data nikdy neopustí serverové prostředí. To je myslím dost podstatná výhoda.

React Server Components v Expo Routeru (beta)

A teď ta asi nejzajímavější novinka — podpora React Server Components (RSC). Místo toho, aby se veškerý kód spouštěl na klientovi, Server Components umožňují vykreslovat části UI na serveru. Díky Expo SDK 55 a React 19.2 to funguje na všech platformách včetně nativních, nejen na webu. Ano, čtete správně.

Jak Server Components fungují v React Native

Server Components se na serveru vykreslí do speciálního serializovaného formátu (ne HTML, ale React-specifického formátu), který se pošle na klienta. Ten ho zpracuje a vykreslí nativní komponenty. Velká výhoda? Kód Server Components nikdy neopustí server — můžete přímo přistupovat k databázi, číst soubory, používat tajné klíče.

// app/articles/page.tsx — Server Component pro seznam článků
// Tento soubor se spouští POUZE na serveru
import { View, Text, StyleSheet } from 'react-native';
import { Link } from 'expo-router';
import { db } from '@/lib/database';

// Server Component — žádné useState, useEffect atd.
export default async function SeznamClanku() {
  const clanky = await db.query(
    'SELECT * FROM clanky ORDER BY vytvoreno DESC LIMIT 20'
  );

  return (
    <View style={styly.kontejner}>
      <Text style={styly.nadpis}>Nejnovější články</Text>
      {clanky.map((clanek) => (
        <Link
          key={clanek.id}
          href={`/articles/${clanek.slug}`}
          style={styly.karticka}
        >
          <View>
            <Text style={styly.nazevClanku}>{clanek.nazev}</Text>
            <Text style={styly.datum}>
              {new Date(clanek.vytvoreno).toLocaleDateString('cs-CZ')}
            </Text>
          </View>
        </Link>
      ))}
    </View>
  );
}

const styly = StyleSheet.create({
  kontejner: { flex: 1, padding: 16 },
  nadpis: { fontSize: 24, fontWeight: 'bold', marginBottom: 16 },
  karticka: { padding: 16, marginBottom: 8, backgroundColor: '#f5f5f5', borderRadius: 8 },
  nazevClanku: { fontSize: 18, fontWeight: '600' },
  datum: { fontSize: 14, color: '#666', marginTop: 4 },
});

Kombinace Server a Client Components

// components/OblibenyTlacitko.tsx — Client Component pro interaktivitu
'use client';

import { useState } from 'react';
import { Pressable, Text, StyleSheet } from 'react-native';

interface Props {
  idClanku: string;
  pocatecniStav: boolean;
}

export function OblibenyTlacitko({ idClanku, pocatecniStav }: Props) {
  const [jeOblibeny, setJeOblibeny] = useState(pocatecniStav);

  async function prepnoutOblibeny() {
    setJeOblibeny((predchozi) => !predchozi);
    await fetch(`/api/articles/${idClanku}/favorite`, {
      method: 'POST',
      body: JSON.stringify({ oblibeny: !jeOblibeny }),
    });
  }

  return (
    <Pressable onPress={prepnoutOblibeny} style={styly.tlacitko}>
      <Text style={styly.text}>
        {jeOblibeny ? '★ Oblíbený' : '☆ Přidat do oblíbených'}
      </Text>
    </Pressable>
  );
}

const styly = StyleSheet.create({
  tlacitko: {
    padding: 12,
    backgroundColor: '#fff3cd',
    borderRadius: 8,
    alignItems: 'center',
  },
  text: { fontSize: 16, fontWeight: '600' },
});

Výhody jsou zřejmé: menší JavaScript bundle (kód Server Components se neposílá na klienta), přímý přístup k datovým zdrojům bez API vrstvy a lepší výkon. Musím ale upozornit, že se pořád jedná o beta funkci, takže API se může ještě měnit.

Navigační vzory a osvědčené postupy

Při návrhu navigační struktury je důležité dodržovat pár osvědčených vzorů. Ušetří vám to spoustu práce při údržbě a vaši uživatelé budou spokojenější.

Programová navigace

Expo Router nabízí několik způsobů programové navigace přes objekt router:

import { router } from 'expo-router';

// Přechod na novou obrazovku (přidání na zásobník)
router.push('/products/42');

// Nahrazení aktuální obrazovky (bez možnosti se vrátit)
router.replace('/login');

// Návrat na předchozí obrazovku
router.back();

// Návrat na kořenovou obrazovku a vyčištění zásobníku
router.dismissAll();

// Navigace s parametry
router.push({
  pathname: '/search',
  params: { query: 'React Native', kategorie: 'kurzy' },
});

// Podmíněná navigace po odeslání formuláře
async function odeslatFormular(data: FormData) {
  try {
    const odpoved = await fetch('/api/submit', {
      method: 'POST',
      body: data,
    });
    if (odpoved.ok) {
      router.replace('/confirmation');
    } else {
      router.push('/error');
    }
  } catch (chyba) {
    console.error('Chyba při odesílání:', chyba);
  }
}

Ošetření chybové stránky

// app/+not-found.tsx — Vlastní stránka 404
import { View, Text, StyleSheet } from 'react-native';
import { Link, Stack } from 'expo-router';

export default function StrankaNenalezena() {
  return (
    <>
      <Stack.Screen options={{ title: 'Stránka nenalezena' }} />
      <View style={styly.kontejner}>
        <Text style={styly.kod}>404</Text>
        <Text style={styly.zprava}>
          Omlouváme se, ale požadovaná stránka nebyla nalezena.
        </Text>
        <Link href="/" style={styly.odkaz}>
          Zpět na domovskou stránku
        </Link>
      </View>
    </>
  );
}

const styly = StyleSheet.create({
  kontejner: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: 20 },
  kod: { fontSize: 72, fontWeight: 'bold', color: '#ccc', marginBottom: 16 },
  zprava: { fontSize: 18, textAlign: 'center', color: '#666', marginBottom: 24 },
  odkaz: { fontSize: 16, color: '#4a90d9', fontWeight: '600' },
});

Organizace velkých projektů

Pro větší aplikace doporučuju tuhle strukturu adresářů — osvědčila se mi v praxi:

app/
├── _layout.tsx                    # Kořenový layout s providery
├── +not-found.tsx                 # Vlastní 404 stránka
├── (auth)/                        # Autentizační skupina
│   ├── _layout.tsx
│   ├── login.tsx
│   ├── register.tsx
│   └── forgot-password.tsx
├── (app)/                         # Hlavní aplikační skupina
│   ├── _layout.tsx                # Záložkový layout
│   ├── (home)/
│   │   ├── _layout.tsx
│   │   ├── index.tsx
│   │   └── notifications.tsx
│   ├── (search)/
│   │   ├── _layout.tsx
│   │   ├── index.tsx
│   │   └── results.tsx
│   └── (profile)/
│       ├── _layout.tsx
│       ├── index.tsx
│       └── edit.tsx
├── products/
│   ├── [id].tsx
│   └── [id]/
│       └── reviews.tsx
└── api/
    ├── auth/
    │   ├── login+api.ts
    │   └── logout+api.ts
    └── products/
        ├── index+api.ts
        └── [id]+api.ts

Tipy pro výkon

Výkon navigace je zásadní pro uživatelský zážitek. Pomalé přechody mezi obrazovkami uživatele frustrují víc, než byste čekali. Tady je pár tipů, jak to řešit.

Líné načítání a optimalizace záložek

// app/(tabs)/_layout.tsx — Optimalizace záložkové navigace
import { Tabs } from 'expo-router';

export default function ZalozkovyLayout() {
  return (
    <Tabs
      screenOptions={{
        lazy: true,            // Načítat obrazovky až při první návštěvě
        freezeOnBlur: true,    // Zmrazit neaktivní záložky
        animation: 'shift',   // Optimalizované animace
      }}
    >
      <Tabs.Screen name="index" options={{ title: 'Domů' }} />
      <Tabs.Screen name="search" options={{ title: 'Hledat' }} />
      <Tabs.Screen name="profile" options={{ title: 'Profil' }} />
    </Tabs>
  );
}

Efektivní seznam s navigací

// app/(tabs)/index.tsx — Efektivní seznam s navigací
import { FlashList } from '@shopify/flash-list';
import { Pressable, Text, View, StyleSheet } from 'react-native';
import { router } from 'expo-router';
import { useCallback, memo } from 'react';

interface Polozka {
  id: string;
  nazev: string;
  popis: string;
}

const PolozkaSeznamu = memo(function PolozkaSeznamu({ polozka }: { polozka: Polozka }) {
  const navigovat = useCallback(() => {
    router.push(`/products/${polozka.id}`);
  }, [polozka.id]);

  return (
    <Pressable onPress={navigovat} style={styly.polozka}>
      <Text style={styly.nazevPolozky}>{polozka.nazev}</Text>
      <Text style={styly.popisPolozky}>{polozka.popis}</Text>
    </Pressable>
  );
});

export default function DomovskaObrazovka() {
  const renderPolozka = useCallback(
    ({ item }: { item: Polozka }) => <PolozkaSeznamu polozka={item} />,
    []
  );

  return (
    <FlashList
      data={data}
      renderItem={renderPolozka}
      estimatedItemSize={80}
      keyExtractor={(polozka) => polozka.id}
    />
  );
}

const styly = StyleSheet.create({
  polozka: { padding: 16, borderBottomWidth: 1, borderBottomColor: '#eee' },
  nazevPolozky: { fontSize: 16, fontWeight: '600' },
  popisPolozky: { fontSize: 14, color: '#666', marginTop: 4 },
});

Další tipy pro výkon

  • Minimalizujte vnořené navigátory — každý přidává režii. Používejte skupiny cest místo zbytečného vnořování.
  • Zapněte freezeOnBlur — zastaví překreslování neviditelných obrazovek a výrazně šetří výkon.
  • Používejte expo-image místo standardní komponenty Image. Podporuje automatické kešování a moderní formáty (WebP, AVIF).
  • Netlačte těžké operace do layoutů — layouty se vykreslují při každé navigaci. Náročné výpočty patří do podřízených komponent.
  • React Compiler — Expo SDK 55 s React 19.2 ho plně podporuje. Automaticky memoizuje komponenty a eliminuje zbytečná překreslení. Stojí to za vyzkoušení.

Migrace z React Navigation

Máte existující projekt na React Navigation? Přechod na Expo Router je jednodušší, než byste čekali, protože Expo Router je postaven přímo na React Navigation. Všechny koncepty mají své přímé protějšky.

Klíčové kroky migrace:

  1. Převeďte navigační strukturu na souborovou — každý Stack.Screen se stane souborem v app/
  2. Navigátory přeměňte na layoutyStack.Navigator se stane _layout.tsx
  3. Vyměňte useNavigation za router — z navigation.navigate('Screen') bude router.push('/screen')
  4. Nahraďte useRoute za useLocalSearchParams — pro přístup k parametrům
  5. Smažte deep linking konfiguraci — s Expo Routerem je automatická, takže ji prostě nepotřebujete

Shrnutí

Expo Router představuje zásadní změnu v tom, jak přemýšlíme o navigaci v React Native. Souborové směrování eliminuje hromadu konfiguračního kódu a přináší konvence z moderních webových frameworků do mobilního vývoje.

Co jsme si prošli:

  • Souborové směrování — adresář app/ definuje celou navigační hierarchii
  • Layout soubory_layout.tsx nahrazuje navigátory z React Navigation
  • Stack, Tabs, Drawer — tři typy layoutů pro všechny běžné scénáře
  • Dynamické cesty[id].tsx pro parametrizované cesty
  • Skupiny cest(auth) pro logickou organizaci bez vlivu na URL
  • Typované cesty — automatické TypeScript typy pro kompilační kontrolu
  • Deep linking — funguje automaticky bez další konfigurace
  • Stack.Protected — řízení přístupu přímo v navigaci
  • API cesty — serverové endpointy přes +api.ts
  • React Server Components — serverové vykreslování na všech platformách (zatím beta)

Expo Router v6.x spolu s Expo SDK 55 je nejkompletnější navigační řešení pro React Native, jaké tu kdy bylo. Souborové směrování, typová bezpečnost, automatický deep linking a podpora Server Components — to všechno dohromady vytváří vývojářský zážitek srovnatelný s nejlepšími webovými frameworky.

Pokud začínáte nový projekt, Expo Router by měl být vaše první volba. A pokud máte existující projekt na React Navigation, migrace je díky sdílenému základu překvapivě snadná. Investice do přechodu se vrátí v podobě lepší udržovatelnosti, snazšího onboardingu nových kolegů a automatické podpory deep linkingu.

React Native se neustále vyvíjí a Expo Router stojí v čele téhle evoluce. S plnou podporou nové architektury, React 19.2 a Server Components je připravený na budoucnost mobilního vývoje — ať už stavíte jednoduchý prototyp nebo složitou produkční aplikaci.

O Autorovi Editorial Team

Our team of expert writers and editors.