Expo Router v6 у 2026: повний гайд з навігації, захищених маршрутів та аутентифікації

Повний гайд з Expo Router v6: файлова маршрутизація, Stack, Tabs, Drawer, захищені маршрути з Stack.Protected, аутентифікація, динамічні маршрути, типізація TypeScript та deep linking. Робочі приклади коду для React Native.

Вступ: чому Expo Router змінив підхід до навігації

Навігація — це, по суті, скелет будь-якого мобільного додатку. Без нормальної навігації навіть найкрасивіший інтерфейс перетворюється на лабіринт, з якого користувач просто піде. React Native довгий час покладався на React Navigation — чудову бібліотеку, але, чесно кажучи, з доволі великим обсягом конфігурації.

Expo Router змінив цю парадигму. Файлова маршрутизація, де структура папок і є навігацією — це саме те, на що всі чекали.

Expo Router v6, випущений разом з SDK 54 у вересні 2025, приніс справжню революцію: нативні вкладки з ефектом Liquid Glass для iOS 26, попередній перегляд посилань, серверний middleware та покращені модальні вікна. А у лютому 2026-го, з виходом бета-версії SDK 55 і React Native 0.83, екосистема стала ще стабільнішою.

У цьому посібнику ми пройдемо весь шлях від базової структури проєкту до складних патернів навігації: стеки, вкладки, бічне меню, захищені маршрути з Stack.Protected, аутентифікація, типізовані маршрути та deep linking. Усі приклади коду — робочі, їх можна одразу використати у вашому проєкті.

Файлова маршрутизація: як структура папок стає навігацією

Ключова ідея Expo Router проста й елегантна: кожен файл у директорії app/ автоматично стає маршрутом у вашому додатку. Якщо ви працювали з Next.js — концепція буде знайомою, але тут вона адаптована під мобільну розробку з нативними переходами між екранами.

Базова структура проєкту

Ось типова структура проєкту з Expo Router v6:

app/
  _layout.tsx          → Кореневий лейаут (точка входу навігації)
  index.tsx            → Головний екран (/)
  about.tsx            → Екран "Про нас" (/about)
  settings.tsx         → Налаштування (/settings)
  user/
    [id].tsx           → Динамічний маршрут (/user/123)
    index.tsx          → Список користувачів (/user)
  (tabs)/
    _layout.tsx        → Лейаут вкладок
    home.tsx           → Вкладка "Головна"
    profile.tsx        → Вкладка "Профіль"
  (auth)/
    sign-in.tsx        → Екран входу
    sign-up.tsx        → Екран реєстрації

Зверніть увагу на кілька важливих конвенцій:

  • _layout.tsx — спеціальний файл, що визначає навігатор для своєї директорії. Це може бути Stack, Tabs або Drawer.
  • index.tsx — маршрут за замовчуванням для директорії (аналог index.html у вебі).
  • [param].tsx — динамічний маршрут, де param буде доступний через хуки.
  • (group) — група маршрутів у дужках. Вона не впливає на URL, але допомагає організувати лейаути.

Кореневий лейаут

Кореневий _layout.tsx — це точка входу усієї навігації. Саме тут ви визначаєте верхній рівень навігатора й глобальні провайдери:

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

export default function RootLayout() {
  return (
    <>
      <StatusBar style="auto" />
      <Stack screenOptions={{ headerShown: false }}>
        <Stack.Screen name="index" />
        <Stack.Screen name="(tabs)" />
        <Stack.Screen
          name="modal"
          options={{ presentation: 'modal' }}
        />
      </Stack>
    </>
  );
}

Stack-навігація: основа переходів між екранами

Stack-навігатор — найпоширеніший тип навігації в мобільних додатках. На iOS новий екран виїжджає справа, на Android — знизу вгору. Кожен наступний екран кладеться "на стек" попередніх, і користувач може повернутися назад жестом або кнопкою.

Це та сама навігація, до якої звикли користувачі будь-якого нативного додатку.

Налаштування Stack

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

export default function RootLayout() {
  return (
    <Stack
      screenOptions={{
        headerStyle: { backgroundColor: '#1a1a2e' },
        headerTintColor: '#e0e0e0',
        headerTitleStyle: { fontWeight: 'bold' },
        animation: 'slide_from_right',
      }}
    >
      <Stack.Screen
        name="index"
        options={{ title: 'Головна' }}
      />
      <Stack.Screen
        name="details"
        options={{ title: 'Деталі' }}
      />
      <Stack.Screen
        name="modal"
        options={{
          presentation: 'modal',
          animation: 'slide_from_bottom',
        }}
      />
    </Stack>
  );
}

Програмна навігація з useRouter

Expo Router надає хук useRouter для програмної навігації. Ось повний набір методів:

import { useRouter } from 'expo-router';

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

  return (
    <View>
      {/* Перехід на новий екран (додає в стек) */}
      <Button
        title="Відкрити деталі"
        onPress={() => router.push('/details')}
      />

      {/* Заміна поточного екрану (не додає в стек) */}
      <Button
        title="Замінити на профіль"
        onPress={() => router.replace('/profile')}
      />

      {/* Навігація з автоматичним визначенням дії */}
      <Button
        title="Перейти до налаштувань"
        onPress={() => router.navigate('/settings')}
      />

      {/* Повернення назад */}
      <Button
        title="Назад"
        onPress={() => {
          if (router.canGoBack()) {
            router.back();
          }
        }}
      />

      {/* Закрити всі екрани до вказаного */}
      <Button
        title="До головної"
        onPress={() => router.dismissTo('/')}
      />
    </View>
  );
}

Різниця між push, navigate і replace — критично важлива штука. push завжди додає новий екран у стек, navigate перевіряє, чи вже є такий маршрут, і або переходить до нього, або додає новий. А replace замінює поточний екран без можливості повернутися назад. На практиці це означає, що після replace кнопка "Назад" не спрацює — майте це на увазі.

Tab-навігація: вкладки для основних розділів

Вкладки — стандартний патерн для мобільних додатків, де нижня панель дає швидкий доступ до основних розділів. Expo Router v6 пропонує три типи вкладок: JavaScript Tabs, Native Tabs та кастомні вкладки.

JavaScript Tabs — класичний підхід

JavaScript Tabs побудовані на React Navigation Bottom Tabs Navigator v7 і дають максимальну гнучкість кастомізації:

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

export default function TabsLayout() {
  return (
    <Tabs
      screenOptions={{
        tabBarActiveTintColor: '#6C63FF',
        tabBarInactiveTintColor: '#8E8E93',
        tabBarStyle: {
          backgroundColor: '#1a1a2e',
          borderTopColor: '#2d2d44',
        },
        headerShown: false,
      }}
    >
      <Tabs.Screen
        name="home"
        options={{
          title: 'Головна',
          tabBarIcon: ({ color, size }) => (
            <Ionicons name="home-outline" color={color} size={size} />
          ),
        }}
      />
      <Tabs.Screen
        name="search"
        options={{
          title: 'Пошук',
          tabBarIcon: ({ color, size }) => (
            <Ionicons name="search-outline" color={color} size={size} />
          ),
        }}
      />
      <Tabs.Screen
        name="profile"
        options={{
          title: 'Профіль',
          tabBarIcon: ({ color, size }) => (
            <Ionicons name="person-outline" color={color} size={size} />
          ),
        }}
      />
      {/* Приховати маршрут з панелі вкладок */}
      <Tabs.Screen
        name="hidden-settings"
        options={{ href: null }}
      />
    </Tabs>
  );
}

Native Tabs — нативний вигляд із Liquid Glass

А ось тут стає по-справжньому цікаво. Expo Router v6 представив Native Tabs — справжні нативні вкладки платформи. На iOS 26 це той самий ефект Liquid Glass, на Android — Material Tabs. API ще в альфі, але вона вже доступна у SDK 54:

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

export default function TabsLayout() {
  return (
    <NativeTabs>
      <NativeTabs.Screen
        name="home"
        options={{
          title: 'Головна',
          tabBarIcon: ({ sf: 'house.fill', md: 'home' }),
        }}
      />
      <NativeTabs.Screen
        name="search"
        options={{
          title: 'Пошук',
          tabBarIcon: ({ sf: 'magnifyingglass', md: 'search' }),
        }}
      />
      <NativeTabs.Screen
        name="profile"
        options={{
          title: 'Профіль',
          tabBarIcon: ({ sf: 'person.fill', md: 'person' }),
          tabBarBadge: '3',
        }}
      />
    </NativeTabs>
  );
}

Є кілька обмежень, які варто знати наперед: усі екрани вкладок рендеряться одразу при монтуванні навігатора (це необхідно для нативних переходів), і динамічне додавання або видалення вкладок під час роботи не підтримується. Вкладки мають бути визначені статично — без варіантів.

Стек всередині вкладок

Найпоширеніший патерн — вкладена Stack-навігація всередині вкладки. Таким чином кожна вкладка може мати свій стек екранів:

// Структура файлів:
// app/(tabs)/feed/
//   _layout.tsx    → Stack для стрічки
//   index.tsx      → Список постів
//   [postId].tsx   → Деталі посту

// app/(tabs)/feed/_layout.tsx
import { Stack } from 'expo-router';

export default function FeedLayout() {
  return (
    <Stack>
      <Stack.Screen
        name="index"
        options={{ title: 'Стрічка' }}
      />
      <Stack.Screen
        name="[postId]"
        options={{ title: 'Пост' }}
      />
    </Stack>
  );
}

// app/(tabs)/feed/[postId].tsx
import { useLocalSearchParams } from 'expo-router';
import { Text, View } from 'react-native';

export default function PostDetails() {
  const { postId } = useLocalSearchParams<{ postId: string }>();

  return (
    <View>
      <Text>Пост #{postId}</Text>
    </View>
  );
}

Такий підхід гарантує, що при переході до деталей поста нижня панель вкладок залишається видимою, а користувач може повернутися назад. Зручно і передбачувано.

Drawer-навігація: бічне меню

Drawer (бічне меню) — ще один класичний патерн навігації. Користувач витягує навігаційну панель свайпом або натискає на кнопку-"бургер". Для Drawer потрібні додаткові залежності:

# Встановлення залежностей
npx expo install @react-navigation/drawer react-native-gesture-handler react-native-reanimated react-native-worklets
// app/_layout.tsx
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { Drawer } from 'expo-router/drawer';

export default function RootLayout() {
  return (
    <GestureHandlerRootView style={{ flex: 1 }}>
      <Drawer
        screenOptions={{
          drawerStyle: {
            backgroundColor: '#1a1a2e',
            width: 280,
          },
          drawerActiveTintColor: '#6C63FF',
          drawerInactiveTintColor: '#8E8E93',
        }}
      >
        <Drawer.Screen
          name="index"
          options={{ drawerLabel: 'Головна', title: 'Головна' }}
        />
        <Drawer.Screen
          name="settings"
          options={{ drawerLabel: 'Налаштування', title: 'Налаштування' }}
        />
      </Drawer>
    </GestureHandlerRootView>
  );
}

Захищені маршрути та аутентифікація

Окей, переходимо до найцікавішого — захисту маршрутів від неавторизованих користувачів. Якщо ви працювали з навігацією до Expo Router v5, то знаєте цей біль: ручні редиректи, перевірки стану аутентифікації в кожному лейауті, перегони при deep linking...

Stack.Protected, що з'явився у v5 і був вдосконалений у v6, вирішує все це декларативно й надійно.

Створення контексту аутентифікації

Спочатку створимо провайдер, який зберігає стан сесії:

// context/AuthContext.tsx
import React, { createContext, useContext, useState, useEffect } from 'react';
import * as SecureStore from 'expo-secure-store';
import { Platform } from 'react-native';

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

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

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

async function getStoredToken(): Promise<string | null> {
  if (Platform.OS === 'web') {
    return localStorage.getItem('session-token');
  }
  return SecureStore.getItemAsync('session-token');
}

async function setStoredToken(token: string | null): Promise<void> {
  if (Platform.OS === 'web') {
    if (token) localStorage.setItem('session-token', token);
    else localStorage.removeItem('session-token');
    return;
  }
  if (token) {
    await SecureStore.setItemAsync('session-token', token);
  } else {
    await SecureStore.deleteItemAsync('session-token');
  }
}

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

  useEffect(() => {
    getStoredToken().then((token) => {
      setSession(token);
      setIsLoading(false);
    });
  }, []);

  const signIn = async (token: string) => {
    await setStoredToken(token);
    setSession(token);
  };

  const signOut = async () => {
    await setStoredToken(null);
    setSession(null);
  };

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

Stack.Protected — декларативний захист

Тепер інтегруємо захищені маршрути у кореневий лейаут. І от тут починається магія:

// app/_layout.tsx
import { Stack } from 'expo-router';
import { SessionProvider, useSession } from '../context/AuthContext';
import { ActivityIndicator, View } from 'react-native';

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

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

  return (
    <Stack screenOptions={{ headerShown: false }}>
      {/* Захищені маршрути — доступні лише авторизованим */}
      <Stack.Protected guard={!!session}>
        <Stack.Screen name="(tabs)" />
        <Stack.Screen
          name="modal"
          options={{ presentation: 'modal' }}
        />
      </Stack.Protected>

      {/* Публічні маршрути — доступні лише неавторизованим */}
      <Stack.Protected guard={!session}>
        <Stack.Screen name="sign-in" />
        <Stack.Screen name="sign-up" />
      </Stack.Protected>
    </Stack>
  );
}

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

Як це працює? Коли session дорівнює null (користувач не увійшов), guard для (tabs) стає false, і маршрутизатор автоматично перенаправляє на перший доступний екран — sign-in. Після входу session отримує значення, guards перемикаються, і користувач потрапляє до вкладок.

І що найкраще — уся історія навігації авторизаційних екранів очищується. Кнопка "Назад" не поверне користувача на sign-in. Це саме та поведінка, яку раніше доводилося реалізовувати вручну.

Tabs.Protected — захист окремих вкладок

Іноді потрібно захистити не весь додаток, а лише деякі вкладки. Скажімо, "Головна" доступна всім, а "Профіль" — тільки авторизованим:

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

export default function TabsLayout() {
  const { session } = useSession();
  const isLoggedIn = !!session;

  return (
    <Tabs>
      {/* Завжди видима вкладка */}
      <Tabs.Screen name="home" options={{ title: 'Головна' }} />

      {/* Вкладки для авторизованих */}
      <Tabs.Protected guard={isLoggedIn}>
        <Tabs.Screen name="profile" options={{ title: 'Профіль' }} />
        <Tabs.Screen name="orders" options={{ title: 'Замовлення' }} />
      </Tabs.Protected>

      {/* Вкладка для неавторизованих */}
      <Tabs.Protected guard={!isLoggedIn}>
        <Tabs.Screen name="login" options={{ title: 'Увійти' }} />
      </Tabs.Protected>
    </Tabs>
  );
}

Рольовий контроль доступу (RBAC)

Stack.Protected підходить не лише для простої аутентифікації. Він чудово працює й для складніших сценаріїв з ролями:

// app/(admin)/_layout.tsx
import { Stack } from 'expo-router';
import { useSession } from '../../context/AuthContext';

export default function AdminLayout() {
  const { user } = useSession();

  return (
    <Stack>
      <Stack.Protected guard={user?.role === 'admin'}>
        <Stack.Screen name="dashboard" />
        <Stack.Screen name="users" />
        <Stack.Screen name="analytics" />
      </Stack.Protected>

      <Stack.Protected guard={user?.role === 'moderator' || user?.role === 'admin'}>
        <Stack.Screen name="moderation" />
        <Stack.Screen name="reports" />
      </Stack.Protected>
    </Stack>
  );
}

Динамічні маршрути та параметри

Динамічні маршрути — це шаблонні сторінки, які обслуговують різний контент залежно від параметрів URL. Замість того щоб створювати окремі файли для /product/1, /product/2, /product/3 (уявіть цей жах), ви створюєте один файл [id].tsx.

Базові динамічні маршрути

// app/product/[id].tsx
import { useLocalSearchParams, useRouter, Link } from 'expo-router';
import { Text, View, ScrollView, Button } from 'react-native';

export default function ProductScreen() {
  const { id } = useLocalSearchParams<{ id: string }>();
  const router = useRouter();

  return (
    <ScrollView>
      <Text>Товар #{id}</Text>

      {/* Навігація з параметрами через Link */}
      <Link
        href={{
          pathname: '/product/[id]',
          params: { id: '42' },
        }}
      >
        Перейти до товару #42
      </Link>

      {/* Програмна навігація з параметрами */}
      <Button
        title="Наступний товар"
        onPress={() => router.push({
          pathname: '/product/[id]',
          params: { id: String(Number(id) + 1) },
        })}
      />
    </ScrollView>
  );
}

Catch-all маршрути

Для обробки маршрутів з довільною кількістю сегментів є синтаксис [...slug]:

// app/docs/[...slug].tsx — обробляє /docs/api/auth/tokens тощо
import { useLocalSearchParams } from 'expo-router';
import { Text } from 'react-native';

export default function DocsScreen() {
  const { slug } = useLocalSearchParams<{ slug: string[] }>();
  // slug = ['api', 'auth', 'tokens'] для /docs/api/auth/tokens

  return <Text>Документація: {slug?.join(' / ')}</Text>;
}

Обробка відсутніх маршрутів

Файл +not-found.tsx обробляє навігацію на неіснуючий маршрут. Це як сторінка 404 у вебі:

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

export default function NotFoundScreen() {
  return (
    <View style={styles.container}>
      <Text style={styles.title}>Сторінку не знайдено</Text>
      <Text style={styles.subtitle}>
        Цієї сторінки не існує або вона була переміщена.
      </Text>
      <Link href="/" style={styles.link}>
        Повернутися на головну
      </Link>
    </View>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: 20 },
  title: { fontSize: 24, fontWeight: 'bold', marginBottom: 8 },
  subtitle: { fontSize: 16, color: '#666', textAlign: 'center', marginBottom: 20 },
  link: { fontSize: 16, color: '#6C63FF' },
});

Типізовані маршрути з TypeScript

Типізовані маршрути — одна з найкорисніших фішок Expo Router, і я не перебільшую. Вони дозволяють TypeScript перевіряти правильність усіх посилань на етапі компіляції. Перейменували файл маршруту? TypeScript одразу покаже помилку у всіх місцях, де є посилання на цей маршрут.

Увімкнення типізованих маршрутів

// app.json
{
  "expo": {
    "experiments": {
      "typedRoutes": true
    }
  }
}

// Після цього запустіть:
// npx expo customize tsconfig.json

Expo CLI автоматично згенерує файл expo-env.d.ts у кореневій директорії проєкту. Його не потрібно редагувати або додавати в git — він регенерується автоматично.

Типобезпечна навігація

import { Link } from 'expo-router';

// TypeScript підтвердить — маршрут існує
<Link href="/about">Про нас</Link>

// Динамічний маршрут із типізованими параметрами
<Link href={{ pathname: '/user/[id]', params: { id: '123' } }}>
  Профіль
</Link>

// Помилка компіляції — маршрут не існує
// <Link href="/nonexistent">Помилка</Link>

// Помилка — невірна назва параметра
// <Link href={{ pathname: '/user/[id]', params: { userId: '123' } }}>
//   Профіль
// </Link>

Deep Linking: кожен екран має URL

Одна з найбільших переваг Expo Router — автоматичний deep linking. Кожен маршрут у вашому додатку автоматично має URL, який можна відкрити ззовні. Для маркетингу, обміну посиланнями та тестування — це просто безцінна можливість.

Як це працює

Expo Router використовує файлову структуру для генерації схеми deep linking. Не потрібно створювати linking.ts або конфігурувати вкладені об'єкти стану — все працює з коробки:

  • app/index.tsxmyapp://
  • app/profile.tsxmyapp://profile
  • app/product/[id].tsxmyapp://product/42

Налаштування Universal Links та App Links

Кастомні URL-схеми (на кшталт myapp://) працюють для базових сценаріїв, але у 2026 році обидві платформи віддають перевагу верифікованим посиланням — Universal Links (iOS) та App Links (Android). Вони використовують стандартні https:// URL і забезпечують захист від перехоплення іншими додатками:

// app.json — налаштування для Universal/App Links
{
  "expo": {
    "scheme": "myapp",
    "ios": {
      "associatedDomains": ["applinks:myapp.example.com"]
    },
    "android": {
      "intentFilters": [
        {
          "action": "VIEW",
          "autoVerify": true,
          "data": [
            {
              "scheme": "https",
              "host": "myapp.example.com",
              "pathPrefix": "/"
            }
          ],
          "category": ["BROWSABLE", "DEFAULT"]
        }
      ]
    }
  }
}

Deep linking із захищеними маршрутами

Взаємодія deep linking та Stack.Protected працює з коробки. Якщо неавторизований користувач відкриває deep link на захищений маршрут, він автоматично потрапляє на sign-in. Але для кращого UX варто зберегти початковий URL і перенаправити після входу:

// hooks/useDeepLinkRedirect.ts
import { useEffect, useRef } from 'react';
import { useRouter, usePathname } from 'expo-router';
import { useSession } from '../context/AuthContext';

export function useDeepLinkRedirect() {
  const { session } = useSession();
  const router = useRouter();
  const pathname = usePathname();
  const pendingRedirect = useRef<string | null>(null);

  useEffect(() => {
    if (!session && pathname !== '/sign-in') {
      pendingRedirect.current = pathname;
    }

    if (session && pendingRedirect.current) {
      const redirect = pendingRedirect.current;
      pendingRedirect.current = null;
      router.replace(redirect);
    }
  }, [session, pathname]);
}

Модальні вікна та складні патерни

Модальні вікна — екрани, що відображаються поверх поточного контенту. Expo Router підтримує нативні модалки (з анімацією знизу вгору), прозорі модалки й навіть веб-модалки з поведінкою iPad:

// app/_layout.tsx — визначення модального маршруту
import { Stack } from 'expo-router';

export default function RootLayout() {
  return (
    <Stack>
      <Stack.Screen name="(tabs)" options={{ headerShown: false }} />
      <Stack.Screen
        name="create-post"
        options={{
          presentation: 'modal',
          title: 'Створити пост',
        }}
      />
      <Stack.Screen
        name="image-preview"
        options={{
          presentation: 'transparentModal',
          animation: 'fade',
          headerShown: false,
        }}
      />
    </Stack>
  );
}

// app/create-post.tsx
import { useRouter } from 'expo-router';
import { View, Text, Button } from 'react-native';

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

  const handleSubmit = async () => {
    // ... логіка збереження
    router.back(); // Закрити модалку
  };

  return (
    <View style={{ flex: 1, padding: 20 }}>
      <Text style={{ fontSize: 24 }}>Новий пост</Text>
      <Button title="Зберегти" onPress={handleSubmit} />
      <Button title="Скасувати" onPress={() => router.back()} />
    </View>
  );
}

Серверний middleware (експериментально)

Expo Router v6 додав експериментальний серверний middleware — код, що виконується на сервері перед обробкою маршруту. Це відкриває можливості для серверної авторизації, логування та перенаправлень. Функціональність ще експериментальна, але вже цілком робоча:

// app/+middleware.ts
import { ExpoRequest, ExpoResponse } from 'expo-router/server';

export async function middleware(request: ExpoRequest) {
  const url = new URL(request.url);

  // Перевірка авторизації для API-маршрутів
  if (url.pathname.startsWith('/api/admin')) {
    const authHeader = request.headers.get('Authorization');
    if (!authHeader || !authHeader.startsWith('Bearer ')) {
      return new ExpoResponse('Unauthorized', { status: 401 });
    }
  }

  // Перенаправлення старих URL
  if (url.pathname === '/old-page') {
    return ExpoResponse.redirect(new URL('/new-page', request.url));
  }
}

Найкращі практики навігації в Expo Router

На основі реального досвіду та офіційних рекомендацій — ось набір порад, які зекономлять вам час і нерви:

  • Завжди визначайте index.tsx у кожній директорії — це запобігає некоректному завантаженню початкового екрану.
  • Виносьте спільні екрани за межі вкладок — це дозволяє уникнути скидання стану вкладки при переході.
  • Використовуйте router.canGoBack() перед router.back() — інакше отримаєте краш, коли стек порожній.
  • Увімкніть типізовані маршрути — помилки навігації краще ловити на компіляції, ніж у продакшені.
  • Не робіть глибоку вкладеність — три рівні навігації (Stack → Tabs → Stack) — це зазвичай максимум. Більша глибина ускладнює і розуміння, і дебагінг.
  • Мінімізуйте кількість груп маршрутів — кожна група (group) створює додатковий рівень лейауту.
  • Для модальних вікон використовуйте presentation: 'modal' у кореневому Stack, а не вкладені навігатори.

FAQ — часті запитання

Чим Expo Router відрізняється від React Navigation?

Expo Router побудований поверх React Navigation і є його розширенням. Ключова відмінність — файлова маршрутизація: маршрути визначаються структурою директорії app/, а не конфігурацією в коді. При цьому всі API React Navigation залишаються доступними. Expo Router додає типізовані маршрути, автоматичний deep linking, API Routes та серверний middleware.

Як працюють захищені маршрути при deep linking?

Stack.Protected автоматично перевіряє guard-умови навіть при відкритті deep link. Якщо неавторизований користувач натискає посилання на захищений екран — його перенаправляє на перший доступний публічний маршрут (як правило, sign-in). Працює без додаткової конфігурації.

Чи можна використовувати Expo Router без Expo?

Ні, Expo Router тісно інтегрований з екосистемою Expo і вимагає Expo CLI та Metro bundler. Для проєктів на "чистому" React Native без Expo краще використовувати React Navigation напряму.

Як мігрувати з React Navigation на Expo Router?

Міграція полягає у перенесенні навігаційних конфігурацій у файлову структуру. Створіть директорію app/, перенесіть екрани як файли відповідно до їхніх маршрутів, замініть NavigationContainer на кореневий _layout.tsx, а конфігурацію навігаторів — на відповідні лейаути. Оскільки Expo Router побудований на React Navigation, більшість API виглядатимуть знайомо.

Чи підтримує Expo Router статичну генерацію для вебу?

Так, підтримує. Expo Router має підтримку статичної генерації (SSG) для веб-платформи, що покращує SEO та швидкість завантаження. Зверніть увагу, що захищені маршрути не генерують HTML під час статичної збірки — це клієнтський механізм захисту.

Про Автора Editorial Team

Our team of expert writers and editors.