Expo Router v6: Kompletný sprievodca navigáciou v React Native

Praktický sprievodca navigáciou v React Native s Expo Router v6 — tabs, drawer, modály, chránené routy, NativeTabs s Liquid Glass, deep linking a pokročilé vzory s ukážkami kódu.

Úvod: Prečo je Expo Router v6 štandardom navigácie v roku 2026

Ak ste v posledných rokoch pracovali s React Native, tak viete, o čom hovorím. Navigácia bývala... no, povedzme si úprimne, poriadna bolesť hlavy. Expo Router v6, vydaný ako súčasť Expo SDK 54, to celé zmenil. A nie len trochu — je to skutočná revolúcia v tom, ako budujeme navigačné štruktúry v mobilných aj webových aplikáciách.

Už žiadne ručné definovanie trás v konfiguračných súboroch. V roku 2026 je file-based routing jednoducho štandard.

Koncept je krásne jednoduchý: súborová štruktúra vášho projektu priamo definuje navigačné cesty aplikácie. Vytvoríte súbor app/settings.tsx a automaticky máte k dispozícii trasu /settings. Žiadna duplicitná konfigurácia, žiadne zabudnuté registrácie obrazoviek. Úprimne, keď som sa k tomu prvýkrát dostal, nechápal som, prečo sme to nerobili takto od začiatku.

Expo Router v6 stojí na pleciach React Navigation — využíva jeho overené navigačné primitívy, ale obaľuje ich do moderného, deklaratívneho API. Medzi kľúčové novinky verzie 6 patria:

  • Natívne taby s Liquid Glass efektom — skutočné systémové záložky na iOS 26 a Androide
  • Vylepšené webové modály — plynulé animácie a gestá na webe pomocou Radix a Vaul
  • Stack.Protected a Tabs.Protected — deklaratívna ochrana routov pre autentifikáciu
  • Link.Preview a Link.Menu — natívne náhľady odkazov a kontextové menu na iOS
  • Server-side middleware — spracovanie požiadaviek pred API routami
  • React 19.1 ako predvolená verzia — s podporou React Server komponentov

V tomto sprievodcovi si prejdeme všetko dôležité — od základného nastavenia cez pokročilé vzory až po riešenie bežných problémov. Takže poďme na to.

Inštalácia a základný setup

Najrýchlejší spôsob, ako začať s Expo Router v6, je vytvoriť nový projekt pomocou oficiálnej šablóny. Otvorte terminál a spustite:

npx create-expo-app@latest moja-aplikacia
cd moja-aplikacia
npx expo start

To je celé. Šablóna už obsahuje Expo Router v6 predkonfigurovaný a pripravený na použitie.

Pozrime sa na štruktúru projektu, ktorú dostanete:

moja-aplikacia/
├── app/
│   ├── _layout.tsx          # Koreňový layout
│   ├── index.tsx             # Domovská obrazovka (/)
│   ├── +not-found.tsx        # 404 stránka
│   └── (tabs)/
│       ├── _layout.tsx       # Layout pre záložky
│       ├── index.tsx         # Prvá záložka
│       └── explore.tsx       # Druhá záložka
├── assets/
├── components/
├── constants/
├── hooks/
├── package.json
└── app.json

Adresár app/ je srdcom vašej aplikácie. Každý súbor v ňom sa automaticky stáva trasou. Špeciálne súbory majú nasledujúci význam:

  • _layout.tsx — definuje navigačný layout pre daný adresár (stack, tabs, drawer)
  • index.tsx — predvolená trasa adresára (mapuje sa na /)
  • +not-found.tsx — zachytáva neexistujúce trasy (404)

V súbore app.json sa uistite, že máte nastavený správny scheme pre deep linking:

{
  "expo": {
    "name": "moja-aplikacia",
    "scheme": "mojaaplikacia",
    "plugins": ["expo-router"],
    "web": {
      "bundler": "metro"
    }
  }
}

Stack navigácia

Stack navigácia je najzákladnejší navigačný vzor — obrazovky sa ukladajú na seba ako karta na kartu. Používateľ sa môže vrátiť späť gestom alebo tlačidlom. Nič prekvapivé, ale funguje to spoľahlivo.

V Expo Router v6 sa stack definuje v súbore _layout.tsx:

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

export default function RootLayout() {
  return (
    <Stack
      screenOptions={{
        headerStyle: { backgroundColor: '#1a1a2e' },
        headerTintColor: '#ffffff',
        headerTitleStyle: { fontWeight: 'bold' },
      }}
    >
      <Stack.Screen
        name="index"
        options={{ title: 'Domov' }}
      />
      <Stack.Screen
        name="detail"
        options={{ title: 'Detail' }}
      />
      <Stack.Screen
        name="(tabs)"
        options={{ headerShown: false }}
      />
    </Stack>
  );
}

Navigácia medzi obrazovkami je jednoduchá — použijete komponent Link alebo hook useRouter:

// app/index.tsx
import { Link, useRouter } from 'expo-router';
import { View, Text, Pressable, StyleSheet } from 'react-native';

export default function HomeScreen() {
  const router = useRouter();

  return (
    <View style={styles.container}>
      <Text style={styles.title}>Vitajte v aplikácii</Text>

      {/* Deklaratívna navigácia pomocou Link */}
      <Link href="/detail" style={styles.link}>
        Otvoriť detail
      </Link>

      {/* Programatická navigácia pomocou router */}
      <Pressable onPress={() => router.push('/detail')}>
        <Text style={styles.button}>Prejsť na detail</Text>
      </Pressable>
    </View>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1, justifyContent: 'center', alignItems: 'center' },
  title: { fontSize: 24, fontWeight: 'bold', marginBottom: 20 },
  link: { color: '#4a90d9', fontSize: 18, marginBottom: 10 },
  button: { color: '#fff', backgroundColor: '#4a90d9', padding: 12, borderRadius: 8 },
});

Osobne preferujem Link pre jednoduchú navigáciu a useRouter pre prípady, keď potrebujem navigovať po nejakej akcii (odoslanie formulára, dokončenie animácie a podobne).

Stack navigátor samozrejme podporuje rôzne animácie prechodov. Môžete ich nastaviť globálne cez screenOptions alebo individuálne pre každú obrazovku:

<Stack.Screen
  name="detail"
  options={{
    title: 'Detail produktu',
    animation: 'slide_from_right',    // alebo 'fade', 'slide_from_bottom'
    headerBackTitle: 'Späť',
    gestureEnabled: true,
  }}
/>

Tab navigácia

Tu to začína byť zaujímavé. Expo Router v6 prináša tri odlišné spôsoby implementácie tab navigácie, každý pre iný prípad použitia. Pozrime sa na všetky tri.

1. JavaScript Tabs (klasické taby)

Toto je tradičný prístup — taby sú renderované v JavaScripte pomocou React Navigation. Máte plnú kontrolu nad vzhľadom a správaním, čo je fajn, keď potrebujete niečo naozaj vlastné:

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

export default function TabLayout() {
  return (
    <Tabs
      screenOptions={{
        tabBarActiveTintColor: '#4a90d9',
        tabBarInactiveTintColor: '#8e8e93',
        tabBarStyle: {
          backgroundColor: '#1a1a2e',
          borderTopWidth: 0,
        },
      }}
    >
      <Tabs.Screen
        name="index"
        options={{
          title: 'Domov',
          tabBarIcon: ({ color, size }) => (
            <Ionicons name="home" color={color} size={size} />
          ),
        }}
      />
      <Tabs.Screen
        name="search"
        options={{
          title: 'Hľadať',
          tabBarIcon: ({ color, size }) => (
            <Ionicons name="search" color={color} size={size} />
          ),
        }}
      />
      <Tabs.Screen
        name="profile"
        options={{
          title: 'Profil',
          tabBarIcon: ({ color, size }) => (
            <Ionicons name="person" color={color} size={size} />
          ),
        }}
      />
    </Tabs>
  );
}

2. NativeTabs (natívne taby s Liquid Glass)

Toto je podľa mňa najväčšia novinka verzie 6. NativeTabs používajú skutočné natívne systémové komponenty — na iOS je to UITabBarController, na Androide natívny Material tab bar. A na iOS 26 automaticky dostanete ten krásny efekt Liquid Glass, ktorý mení vzhľad na základe obsahu pod tab barom.

Dôležité: NativeTabs sú momentálne v alpha fáze a importujú sa z expo-router/unstable-native-tabs. API sa môže zmeniť pred stabilným vydaním. A platí tiež limit maximálne 5 záložiek — čo by ale v praxi nemal byť problém.

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

export default function TabLayout() {
  return (
    <NativeTabs>
      <NativeTabs.Trigger name="index">
        <NativeTabs.Trigger.Icon sf="house.fill" md="home" />
        <NativeTabs.Trigger.Label>Domov</NativeTabs.Trigger.Label>
      </NativeTabs.Trigger>

      <NativeTabs.Trigger name="search">
        <NativeTabs.Trigger.Icon sf="magnifyingglass" md="search" />
        <NativeTabs.Trigger.Label>Hľadať</NativeTabs.Trigger.Label>
      </NativeTabs.Trigger>

      <NativeTabs.Trigger name="messages">
        <NativeTabs.Trigger.Icon sf="message.fill" md="chat" />
        <NativeTabs.Trigger.Label>Správy</NativeTabs.Trigger.Label>
        <NativeTabs.Trigger.Badge>3</NativeTabs.Trigger.Badge>
      </NativeTabs.Trigger>

      <NativeTabs.Trigger name="profile">
        <NativeTabs.Trigger.Icon sf="person.fill" md="person" />
        <NativeTabs.Trigger.Label>Profil</NativeTabs.Trigger.Label>
      </NativeTabs.Trigger>
    </NativeTabs>
  );
}

API pre NativeTabs je založené na koncepte Trigger, čo vám dáva lepšiu kontrolu nad tým, ktoré taby sa zobrazujú. Každý Trigger podporuje tieto vnorené komponenty:

  • NativeTabs.Trigger.Icon — ikona záložky. Prop sf pre SF Symbols na iOS, md pre Material ikony na Androide, src pre vlastné obrázky
  • NativeTabs.Trigger.Label — textový popis záložky. Podporuje prop hidden na skrytie
  • NativeTabs.Trigger.Badge — odznačik pre notifikácie. Bez textu zobrazí len bodku

3. Custom Tabs (vlastné headless taby)

A potom je tu tretia možnosť. Ak potrebujete úplnú kontrolu nad dizajnom (napríklad ten trendy zaoblený floating tab bar), použite headless taby z modulu expo-router/ui. Tieto komponenty nemajú žiadny predvolený štýl — je to čistý blank canvas:

// app/(tabs)/_layout.tsx
import { Tabs, TabList, TabTrigger, TabSlot } from 'expo-router/ui';
import { View, Text, StyleSheet } from 'react-native';

export default function CustomTabLayout() {
  return (
    <Tabs>
      <View style={styles.container}>
        <TabSlot />
        <TabList style={styles.tabBar}>
          <TabTrigger name="home" href="/" style={styles.tab}>
            <Text style={styles.tabText}>Domov</Text>
          </TabTrigger>
          <TabTrigger name="explore" href="/explore" style={styles.tab}>
            <Text style={styles.tabText}>Objavovať</Text>
          </TabTrigger>
          <TabTrigger name="profile" href="/profile" style={styles.tab}>
            <Text style={styles.tabText}>Profil</Text>
          </TabTrigger>
        </TabList>
      </View>
    </Tabs>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1 },
  tabBar: {
    flexDirection: 'row',
    justifyContent: 'space-around',
    backgroundColor: '#1a1a2e',
    paddingVertical: 12,
    paddingBottom: 28,
  },
  tab: { alignItems: 'center', padding: 8 },
  tabText: { color: '#ffffff', fontSize: 14 },
});

Custom taby sú ideálne pre aplikácie s neštandardným designom — napríklad zaoblený floating tab bar, bočný panel na tabletoch alebo taby s animáciami. V praxi ich ale používam zriedkavejšie, lebo NativeTabs pokrývajú väčšinu prípadov.

Drawer navigácia

Drawer (zásuvková) navigácia je klasický vzor pre mobilné aplikácie — menu, ktoré sa vysúva z boku obrazovky. Na rozdiel od stack a tab navigácie vyžaduje inštaláciu niekoľkých ďalších závislostí.

Inštalácia závislostí

npx expo install @react-navigation/drawer react-native-gesture-handler react-native-reanimated

Babel plugin pre Reanimated sa automaticky konfiguruje cez babel-preset-expo, takže nie je potrebná žiadna ďalšia konfigurácia. Jedna vec menej na riešenie.

Nastavenie drawer layoutu

// app/_layout.tsx
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { Drawer } from 'expo-router/drawer';
import { Ionicons } from '@expo/vector-icons';

export default function RootLayout() {
  return (
    <GestureHandlerRootView style={{ flex: 1 }}>
      <Drawer
        screenOptions={{
          drawerActiveTintColor: '#4a90d9',
          drawerInactiveTintColor: '#8e8e93',
          headerStyle: { backgroundColor: '#1a1a2e' },
          headerTintColor: '#ffffff',
          drawerStyle: { backgroundColor: '#16213e' },
          drawerLabelStyle: { color: '#ffffff' },
        }}
      >
        <Drawer.Screen
          name="index"
          options={{
            drawerLabel: 'Domov',
            title: 'Domovská stránka',
            drawerIcon: ({ color, size }) => (
              <Ionicons name="home-outline" color={color} size={size} />
            ),
          }}
        />
        <Drawer.Screen
          name="settings"
          options={{
            drawerLabel: 'Nastavenia',
            title: 'Nastavenia',
            drawerIcon: ({ color, size }) => (
              <Ionicons name="settings-outline" color={color} size={size} />
            ),
          }}
        />
        <Drawer.Screen
          name="about"
          options={{
            drawerLabel: 'O aplikácii',
            title: 'O aplikácii',
            drawerIcon: ({ color, size }) => (
              <Ionicons name="information-circle-outline" color={color} size={size} />
            ),
          }}
        />
      </Drawer>
    </GestureHandlerRootView>
  );
}

Programatické ovládanie draweru

Pre otvorenie a zatvorenie draweru z kódu použite hook useNavigation():

// app/index.tsx
import { useNavigation } from 'expo-router';
import { View, Text, Pressable, StyleSheet } from 'react-native';
import { DrawerActions } from '@react-navigation/native';

export default function HomeScreen() {
  const navigation = useNavigation();

  const openDrawer = () => {
    navigation.dispatch(DrawerActions.openDrawer());
  };

  const toggleDrawer = () => {
    navigation.dispatch(DrawerActions.toggleDrawer());
  };

  return (
    <View style={styles.container}>
      <Pressable onPress={openDrawer} style={styles.button}>
        <Text style={styles.buttonText}>Otvoriť menu</Text>
      </Pressable>

      <Pressable onPress={toggleDrawer} style={styles.button}>
        <Text style={styles.buttonText}>Prepnúť menu</Text>
      </Pressable>
    </View>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1, justifyContent: 'center', alignItems: 'center' },
  button: { backgroundColor: '#4a90d9', padding: 14, borderRadius: 8, marginVertical: 8 },
  buttonText: { color: '#ffffff', fontSize: 16, fontWeight: '600' },
});

Modálna navigácia

Modály sú obrazovky, ktoré sa zobrazia nad aktuálnym obsahom — typicky vysunutím zdola. Ak ste niekedy implementovali modály v React Native ručne, viete, aké to vie byť otravné. Expo Router v6 to celé výrazne zjednodušil.

Základný modál

Na zobrazenie obrazovky ako modálu stačí nastaviť presentation: 'modal' v layout súbore:

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

export default function RootLayout() {
  return (
    <Stack>
      <Stack.Screen name="index" options={{ title: 'Domov' }} />
      <Stack.Screen name="(tabs)" options={{ headerShown: false }} />
      <Stack.Screen
        name="modal"
        options={{
          presentation: 'modal',
          title: 'Nastavenia',
          headerStyle: { backgroundColor: '#2a2a3e' },
          headerTintColor: '#ffffff',
        }}
      />
      <Stack.Screen
        name="form-sheet"
        options={{
          presentation: 'formSheet',
          title: 'Nový príspevok',
          sheetCornerRadius: 20,
          sheetGrabberVisible: true,
        }}
      />
    </Stack>
  );
}

Samotná modálna obrazovka je bežný komponent — nič špeciálne:

// app/modal.tsx
import { useRouter } from 'expo-router';
import { View, Text, Pressable, StyleSheet } from 'react-native';

export default function ModalScreen() {
  const router = useRouter();

  return (
    <View style={styles.container}>
      <Text style={styles.title}>Toto je modálna obrazovka</Text>
      <Text style={styles.description}>
        Na iOS ju zatvoríte potiahnutím nadol.
        Na Androide použite tlačidlo späť.
      </Text>

      <Pressable
        onPress={() => {
          if (router.canGoBack()) {
            router.back();
          }
        }}
        style={styles.closeButton}
      >
        <Text style={styles.closeText}>Zavrieť</Text>
      </Pressable>
    </View>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: 20 },
  title: { fontSize: 22, fontWeight: 'bold', marginBottom: 12 },
  description: { fontSize: 16, textAlign: 'center', color: '#666', marginBottom: 24 },
  closeButton: { backgroundColor: '#e74c3c', paddingHorizontal: 24, paddingVertical: 12, borderRadius: 8 },
  closeText: { color: '#fff', fontSize: 16, fontWeight: '600' },
});

Vylepšené webové modály v Expo Router v6

Verzia 6 prináša zásadné vylepšenie modálov na webe. Modály teraz emulujú animácie a gestá z natívnych platforiem — na desktope sa správajú ako na iPade, na mobile ako na iPhone. V pozadí využívajú knižnice Radix a Vaul, čo je podľa mňa výborná voľba.

Pre aktiváciu webových modálov nastavte premennú prostredia:

# .env
EXPO_UNSTABLE_WEB_MODAL=1

Podporované typy prezentácie zahŕňajú modal, formSheet, transparentModal a containedTransparentModal. Pre správnu navigáciu pri deep linkoch nezabudnite nastaviť anchor:

// app/_layout.tsx
export const unstable_settings = {
  anchor: 'index',  // Základná obrazovka za modálom
};

Chránené routy a autentifikácia

Toto je téma, kde Expo Router v6 naozaj žiari. Nové komponenty Stack.Protected a Tabs.Protected riešia autentifikáciu elegantne a deklaratívne. Koniec chaotických ručných presmerovaní, ktoré boli zdrojom toľkých chýb v predchádzajúcich verziách.

Princíp fungovania

Komponent Protected prijíma prop guard — keď je guard={true}, obrazovky vnútri sú prístupné. Keď je guard={false}, navigácia na ne zlyhá a router automaticky presmeruje na prvú dostupnú obrazovku.

A čo je dôležité — tento mechanizmus funguje aj pri deep linkoch. Ak sa neautentifikovaný používateľ pokúsi otvoriť chránenú URL, bude presmerovaný. To je niečo, čo predtým vyžadovalo dosť komplikované riešenia.

Session context provider

Najprv si vytvoríme kontext pre správu relácie:

// context/session.tsx
import React, { createContext, useContext, useState, useCallback } from 'react';

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

const SessionContext = createContext<SessionContextType>({
  session: null,
  isLoading: false,
  signIn: () => {},
  signOut: () => {},
});

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

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

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

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

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

Dual guard pattern pre Stack navigátor

Kľúčovým vzorom je použitie dvoch Protected blokov — jeden pre prihlásených a druhý pre neprihlásených používateľov. Je to prekvapivo jednoduché:

// app/_layout.tsx
import { Stack } from 'expo-router';
import { SessionProvider, useSession } from '@/context/session';

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

  return (
    <Stack screenOptions={{ headerShown: false }}>
      {/* Obrazovky pre prihlásených používateľov */}
      <Stack.Protected guard={!!session}>
        <Stack.Screen name="(tabs)" />
        <Stack.Screen
          name="modal"
          options={{ presentation: 'modal', headerShown: true }}
        />
      </Stack.Protected>

      {/* Obrazovky pre neprihlásených používateľov */}
      <Stack.Protected guard={!session}>
        <Stack.Screen name="sign-in" />
        <Stack.Screen name="create-account" />
      </Stack.Protected>
    </Stack>
  );
}

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

Protected taby

Rovnaký princíp funguje aj pre tab navigáciu. Môžete podmienene zobrazovať alebo skrývať záložky na základe stavu prihlásenia:

// app/(tabs)/_layout.tsx
import { Tabs } from 'expo-router';
import { useSession } from '@/context/session';

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

  return (
    <Tabs>
      <Tabs.Screen
        name="index"
        options={{ tabBarLabel: 'Domov' }}
      />
      <Tabs.Screen
        name="explore"
        options={{ tabBarLabel: 'Objavovať' }}
      />

      {/* Záložka profilu len pre prihlásených */}
      <Tabs.Protected guard={!!session}>
        <Tabs.Screen
          name="profile"
          options={{ tabBarLabel: 'Profil' }}
        />
        <Tabs.Screen
          name="settings"
          options={{ tabBarLabel: 'Nastavenia' }}
        />
      </Tabs.Protected>

      {/* Záložka prihlásenia len pre neprihlásených */}
      <Tabs.Protected guard={!session}>
        <Tabs.Screen
          name="login"
          options={{ tabBarLabel: 'Prihlásiť sa' }}
        />
      </Tabs.Protected>
    </Tabs>
  );
}

Dôležité upozornenie: Chránené routy sa vyhodnocujú iba na strane klienta. Počas statického generovania sa pre chránené trasy nevytvárajú HTML súbory. Ak však používatelia poznajú URL, môžu priamo požiadať o JavaScript súbory. Takže chránené routy nie sú náhradou za serverovú autentifikáciu — na to nezabúdajte.

Deep linking

Jednou z najväčších výhod Expo Routeru je, že deep linking funguje automaticky. Pretože trasy sú definované súborovou štruktúrou, mapovanie medzi URL a obrazovkami je okamžité. Žiadna manuálna konfigurácia. Seriózne, je to tak jednoduché.

Ako súborová štruktúra mapuje na URL

app/
├── index.tsx              →  /
├── about.tsx              →  /about
├── blog/
│   ├── index.tsx          →  /blog
│   └── [slug].tsx         →  /blog/moj-clanok
├── (tabs)/
│   ├── _layout.tsx        →  (nemá URL - je to layout)
│   ├── index.tsx          →  /
│   └── settings.tsx       →  /settings
└── user/
    └── [id].tsx           →  /user/123

Skupiny v zátvorkách (tabs) sa nezobrazujú v URL. Dynamické segmenty [slug] a [id] akceptujú ľubovoľnú hodnotu.

Nastavenie scheme pre natívne deep linky

Pre fungovanie deep linkov na mobilných zariadeniach potrebujete definovať scheme v app.json:

{
  "expo": {
    "scheme": "mojaaplikacia"
  }
}

Po nastavení bude vaša aplikácia reagovať na URL ako mojaaplikacia://blog/moj-clanok alebo mojaaplikacia://user/123. Jednoduché, nie?

Universal links (iOS) a App Links (Android)

Pre produkčné nasadenie je odporúčané používať universal links, ktoré fungujú cez HTTPS. Konfigurácia vyžaduje nastavenie intentFilters (Android) a associatedDomains (iOS) v app.json:

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

S touto konfiguráciou bude odkaz https://www.mojastranka.sk/blog/moj-clanok automaticky otvorený vo vašej aplikácii (ak ju má používateľ nainštalovanú). Je to trochu viac roboty na nastavenie, ale pre produkčné aplikácie sa to jednoznačne oplatí.

Pokročilé vzory

Vnorené navigátory: Taby vnútri stacku

Najbežnejší pokročilý vzor je stack navigátor, ktorý obsahuje tab navigátor ako jednu z obrazoviek. Stack potom umožňuje otvárať ďalšie obrazovky cez celé rozhranie (vrátane tabov). V praxi to vyzerá takto:

app/
├── _layout.tsx            # Stack navigátor (koreň)
├── (tabs)/
│   ├── _layout.tsx        # Tab navigátor
│   ├── index.tsx          # Tab: Domov
│   ├── search.tsx         # Tab: Hľadať
│   └── profile.tsx        # Tab: Profil
├── product/
│   └── [id].tsx           # Detail produktu (cez celú obrazovku)
├── modal.tsx              # Modálna obrazovka
└── +not-found.tsx         # 404 stránka

Koreňový layout:

// 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={{ title: 'Produkt' }} />
      <Stack.Screen name="modal" options={{ presentation: 'modal' }} />
    </Stack>
  );
}

Dynamické routy s [id].tsx

Dynamické segmenty umožňujú jednej obrazovke obsluhovať rôzne dáta na základe parametra v URL. Toto je niečo, čo budete používať neustále:

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

interface Product {
  id: string;
  name: string;
  price: number;
  description: string;
}

export default function ProductDetail() {
  const { id } = useLocalSearchParams<{ id: string }>();
  const [product, setProduct] = useState<Product | null>(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    // Načítanie dát produktu podľa ID
    fetchProduct(id).then((data) => {
      setProduct(data);
      setLoading(false);
    });
  }, [id]);

  if (loading) {
    return <ActivityIndicator size="large" style={styles.loader} />;
  }

  return (
    <View style={styles.container}>
      <Text style={styles.name}>{product?.name}</Text>
      <Text style={styles.price}>{product?.price} &euro;</Text>
      <Text style={styles.description}>{product?.description}</Text>
    </View>
  );
}

async function fetchProduct(id: string): Promise<Product> {
  const response = await fetch(`https://api.example.com/products/${id}`);
  return response.json();
}

const styles = StyleSheet.create({
  container: { flex: 1, padding: 20 },
  loader: { flex: 1, justifyContent: 'center' },
  name: { fontSize: 24, fontWeight: 'bold', marginBottom: 8 },
  price: { fontSize: 20, color: '#4a90d9', marginBottom: 16 },
  description: { fontSize: 16, lineHeight: 24, color: '#555' },
});

Route groups s (zátvorkami)

Route groups organizujú trasy bez toho, aby ovplyvňovali URL. Bežné použitie? Oddelenie autentifikačných a aplikačných obrazoviek:

app/
├── _layout.tsx
├── (auth)/
│   ├── _layout.tsx        # Stack layout pre auth obrazovky
│   ├── sign-in.tsx        →  /sign-in
│   └── register.tsx       →  /register
├── (app)/
│   ├── _layout.tsx        # Tab layout pre hlavnú aplikáciu
│   ├── index.tsx          →  /
│   ├── feed.tsx           →  /feed
│   └── profile.tsx        →  /profile
└── (app,auth)/
    └── terms.tsx          →  /terms (zdieľaný route)

Všimnite si posledný riadok — syntax (app,auth) vytvorí zdieľaný route, ktorý je prístupný v oboch skupinách. Súbor terms.tsx sa fyzicky nachádza na jednom mieste, ale existuje v kontexte oboch layoutov. Dosť šikovné riešenie.

Zdieľané routy (Array syntax)

Ak potrebujete, aby rovnaká obrazovka existovala v rôznych navigačných kontextoch, použite array syntax v názve adresára:

app/
├── (home,search,profile)/
│   └── [user].tsx         # Profil používateľa dostupný zo všetkých skupín

Toto v pamäti vytvorí tri verzie rovnakého súboru — jednu pre každú skupinu. URL zostáva rovnaká (/meno-pouzivatela), ale navigačný kontext sa líši.

Bežné problémy a ich riešenia

Teraz k tej časti, kvôli ktorej sem veľa z vás pravdepodobne prišlo. Poďme si prejsť najčastejšie problémy, na ktoré narazíte.

1. Resetovanie tabov pri navigácii

Bežný problém: keď sa používateľ vráti na tab, očakáva, že uvidí koreňovú obrazovku, nie poslednú navštívenú. NativeTabs tento problém riešia natívne (pop-to-root pri opakovanom ťuknutí). Pre JavaScript taby to treba doriešiť manuálne:

// V tab layoute
<Tabs.Screen
  name="index"
  options={{
    tabBarLabel: 'Domov',
  }}
  listeners={({ navigation }) => ({
    tabPress: (e) => {
      // Resetovanie stacku pri ťuknutí na aktívny tab
      navigation.popToTop();
    },
  })}
/>

2. Správanie tlačidla späť na Androide

Android má hardvérové tlačidlo späť, čo môže spôsobiť neočakávané správanie. Toto je klasika, s ktorou sa stretne asi každý Android vývojár. Pre kontrolu nad ním použite BackHandler:

import { useEffect } from 'react';
import { BackHandler } from 'react-native';
import { useRouter } from 'expo-router';

export function useCustomBackHandler() {
  const router = useRouter();

  useEffect(() => {
    const handler = BackHandler.addEventListener('hardwareBackPress', () => {
      if (router.canGoBack()) {
        router.back();
        return true;  // Spracované - nepredávať ďalej
      }
      return false;   // Nechať predvolené správanie (ukončenie aplikácie)
    });

    return () => handler.remove();
  }, [router]);
}

3. Route +not-found.tsx

Vždy definujte fallback pre neexistujúce trasy. Expo Router ho zobrazí s HTTP statusom 404. Aj keď sa to zdá samozrejmé, videl som dosť projektov, kde na to zabudli:

// app/+not-found.tsx
import { Link, Stack } from 'expo-router';
import { View, Text, StyleSheet } from 'react-native';

export default function NotFoundScreen() {
  return (
    <>
      <Stack.Screen options={{ title: 'Stránka nenájdená' }} />
      <View style={styles.container}>
        <Text style={styles.emoji}>404</Text>
        <Text style={styles.title}>Táto stránka neexistuje</Text>
        <Text style={styles.description}>
          Odkaz, na ktorý ste klikli, nevedie na žiadnu existujúcu
          obrazovku v aplikácii.
        </Text>
        <Link href="/" style={styles.link}>
          Vrátiť sa na domovskú stránku
        </Link>
      </View>
    </>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: 20 },
  emoji: { fontSize: 64, fontWeight: 'bold', color: '#e74c3c', marginBottom: 16 },
  title: { fontSize: 22, fontWeight: 'bold', marginBottom: 8, textAlign: 'center' },
  description: { fontSize: 16, color: '#666', textAlign: 'center', marginBottom: 24 },
  link: { color: '#4a90d9', fontSize: 18 },
});

4. Flickering v dark mode s Liquid Glass

Ak používate NativeTabs s Liquid Glass na iOS 26 a pozorujete blikanie pri prepínaní tabov v dark mode, zabaľte váš layout do ThemeProvider. Toto je jedno z tých riešení, ktoré sú jednoduché, keď už o nich viete:

import { ThemeProvider, DarkTheme } from '@react-navigation/native';
import { useColorScheme } from 'react-native';

export default function RootLayout() {
  const colorScheme = useColorScheme();
  const theme = colorScheme === 'dark' ? DarkTheme : DefaultTheme;

  return (
    <ThemeProvider value={theme}>
      {/* Váš navigačný layout */}
    </ThemeProvider>
  );
}

5. Nemixujte Tabs a NativeTabs

Nikdy nepoužívajte <Tabs> a <NativeTabs> v rovnakom navigačnom strome. Táto kombinácia môže (a pravdepodobne aj bude) spôsobiť pád aplikácie. Vyberte si jeden prístup a dodržiavajte ho v celom projekte.

Často kladené otázky (FAQ)

Aký je rozdiel medzi Expo Router a React Navigation?

Expo Router nie je náhrada React Navigation — je to nadstavba nad ním. React Navigation poskytuje nízkoúrovňové navigačné primitívy (stack, tab, drawer navigátory), zatiaľ čo Expo Router pridáva file-based routing, automatický deep linking, typovú bezpečnosť trás a optimalizácie pre web.

Ak poznáte React Navigation, Expo Router vám bude ihneď známy — všetky možnosti konfigurácie (screenOptions, options) fungujú rovnako. Hlavný rozdiel je v tom, ako definujete trasy — namiesto imperatívnej konfigurácie používate konvencie súborovej štruktúry.

Ako implementovať autentifikáciu s Expo Router v6?

Najodporúčanejší prístup je použitie Stack.Protected a Tabs.Protected s dual guard vzorom. Vytvorte si session context provider, ktorý spravuje stav prihlásenia, a v koreňovom layoute definujte dva bloky Protected — jeden s guard={!!session} pre chránené obrazovky a druhý s guard={!session} pre prihlasovanie.

Keď sa stav relácie zmení, router automaticky presmeruje používateľa na správnu skupinu obrazoviek. A áno, funguje to aj s deep linkmi.

Môžem používať Expo Router bez Expo?

Nie, naozaj nie. Expo Router je navrhnutý pre ekosystém Expo a závisí na Metro bundleri s podporou RequireContext pre automatické rozpoznávanie trás. Ak nechcete používať Expo, vašou najlepšou alternatívou je priamo React Navigation.

Ale úprimne — Expo je v roku 2026 odporúčaný prístup pre väčšinu React Native projektov a integrácia s existujúcimi natívnymi projektmi je veľmi dobre zdokumentovaná. Tak prečo to neskúsiť?

Ako funguje deep linking s Expo Router?

Automaticky, vďaka file-based routingu. Každý súbor v adresári app/ sa mapuje na URL — napríklad app/blog/[slug].tsx zodpovedá URL /blog/moj-clanok. Pre natívne aplikácie stačí nastaviť scheme v app.json a vaša aplikácia začne reagovať na custom URL scheme.

Pre produkčné prostredie odporúčam nastaviť universal links (iOS) a app links (Android), ktoré fungujú cez štandardné HTTPS URL. S chránenými routami je deep linking bezpečný — ak používateľ nemá prístup k cieľovej obrazovke, bude automaticky presmerovaný.

Aký je rozdiel medzi NativeTabs a JavaScript Tabs?

JavaScript Tabs (Tabs z expo-router) sú tradičné taby implementované kompletne v JavaScripte pomocou React Navigation. Máte nad nimi plnú kontrolu — môžete prispôsobiť každý pixel, pridať vlastné animácie a nemáte žiadne obmedzenia na počet záložiek.

NativeTabs (expo-router/unstable-native-tabs) používajú skutočné natívne systémové komponenty — UITabBarController na iOS a natívny Material tab bar na Androide. Na iOS 26 automaticky získate Liquid Glass efekt. Výhody: natívny scroll-to-top pri ťuknutí, pop-to-root správanie a platformovo špecifické funkcie.

Obmedzenia NativeTabs: maximálne 5 záložiek, API je ešte v alpha fáze a nemáte takú flexibilitu v prispôsobení dizajnu. Napriek tomu — pre väčšinu aplikácií v roku 2026 ich odporúčam. Natívny vzhľad a správanie jednoducho výrazne zvyšuje kvalitu používateľského zážitku.

O Autorovi Editorial Team

Our team of expert writers and editors.