Expo Router: Пълно ръководство за навигация в React Native

Научете как да използвате Expo Router v4 за файлово-базирана навигация в React Native. Практически примери за layouts, tabs, drawer навигация, аутентикация, динамични маршрути, deep linking и TypeScript.

Защо Expo Router промени начина, по който навигираме в React Native

Ако сте работили с React Native навигация, знаете колко болезнен може да бъде целият процес. Конфигуриране на стекове, вложени навигатори, ръчно дефиниране на маршрути, управление на типове — честно казано, всичко това бързо излиза извън контрол. Expo Router решава тези проблеми по доста елегантен начин, заимствайки идеята за файлово-базирано маршрутизиране от уеб фреймуърки като Next.js и Remix.

С Expo Router вашата файлова структура е вашата навигация. Създавате файл в app/ — и автоматично получавате маршрут. Толкова просто е. Няма ръчна конфигурация, няма забравени маршрути, няма разминаване между кода и навигационната карта.

В тази статия ще разгледаме подробно как работи Expo Router — от основната настройка, през layouts и tab навигация, до аутентикация, типизирани маршрути и deep linking. Всички примери са тествани с Expo SDK 53 и Expo Router v4.

Инсталация и начална настройка

Нов проект с Expo Router

Най-лесният начин да стартирате е с шаблона на Expo:

npx create-expo-app@latest my-app --template tabs
cd my-app
npx expo start

Този шаблон ви дава проект с вече конфигуриран Expo Router, tab навигация и примерни екрани — готово за тестване за секунди. Ако предпочитате минимална настройка обаче:

npx create-expo-app@latest my-app --template blank
cd my-app
npx expo install expo-router expo-linking expo-constants expo-status-bar

Конфигурация на app.json

Expo Router изисква правилна конфигурация на входната точка и deep linking схемата. Нищо сложно, просто добавете следното:

{
  "expo": {
    "scheme": "myapp",
    "web": {
      "bundler": "metro",
      "output": "server"
    },
    "plugins": ["expo-router"]
  }
}

Входна точка

В package.json задайте входната точка на expo-router/entry:

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

Това казва на Metro bundler да използва Expo Router за зареждане на приложението. Рутерът автоматично сканира директорията app/ и генерира навигационната структура вместо вас.

Файловата система като навигация — основни конвенции

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

Тук е магията. В Expo Router всеки файл в директорията app/ се превръща в маршрут. Ето как файловата структура се превежда директно в URL пътища:

app/
├── _layout.tsx        → Основен layout
├── index.tsx          → /
├── about.tsx          → /about
├── settings.tsx       → /settings
├── profile/
│   ├── _layout.tsx    → Layout за /profile/*
│   ├── index.tsx      → /profile
│   └── edit.tsx       → /profile/edit
└── blog/
    ├── _layout.tsx    → Layout за /blog/*
    ├── index.tsx      → /blog
    └── [id].tsx       → /blog/123, /blog/abc

Правилата са прости и предвидими:

  • index.tsx — маршрутът по подразбиране за директорията
  • _layout.tsx — layout компонент, обвива дъщерните маршрути
  • [param].tsx — динамичен сегмент (параметър в URL)
  • [...rest].tsx — catch-all маршрут (хваща всички оставащи сегменти)
  • (group) — група за организация, без промяна на URL
  • +not-found.tsx — персонализирана 404 страница

Файлове, които НЕ стават маршрути

Не всеки файл в app/ се превръща в маршрут, разбира се. Expo Router игнорира:

  • Файлове с имена, започващи с долна черта (_layout.tsx, _utils.ts)
  • Файлове в директории, започващи с долна черта (app/_components/)
  • Тестови файлове (*.test.tsx, *.spec.tsx)

Това означава, че спокойно можете да организирате помощни файлове и компоненти вътре в app/, без да замърсявате навигацията. Доста удобно.

Layouts — скелетът на навигацията

Root Layout

Файлът app/_layout.tsx е коренният layout на приложението. Тук зареждате шрифтове, конфигурирате теми и обвивате всичко с нужните providers:

import { Stack } from 'expo-router';
import { ThemeProvider, DarkTheme, DefaultTheme } from '@react-navigation/native';
import { useColorScheme } from 'react-native';
import { useFonts } from 'expo-font';
import * as SplashScreen from 'expo-splash-screen';
import { useEffect } from 'react';

SplashScreen.preventAutoHideAsync();

export default function RootLayout() {
  const colorScheme = useColorScheme();
  const [fontsLoaded] = useFonts({
    'Inter-Regular': require('../assets/fonts/Inter-Regular.ttf'),
    'Inter-Bold': require('../assets/fonts/Inter-Bold.ttf'),
  });

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

  if (!fontsLoaded) return null;

  return (
    
      
        
        
        
      
    
  );
}

Tab Navigation Layout

За приложения с долна навигационна лента (а това са повечето мобилни приложения, нека бъдем честни) създайте група (tabs) с layout:

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

export default function TabLayout() {
  return (
    
       (
            
          ),
        }}
      />
       (
            
          ),
        }}
      />
       (
            
          ),
        }}
      />
    
  );
}

Вложени Layouts

Можете да влагате layouts колкото е необходимо. Например профилният раздел може да си има собствен Stack навигатор:

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

export default function ProfileLayout() {
  return (
    
      
      
      
    
  );
}

Навигация между екрани — Link, router и useRouter

Компонентът Link

Най-простият начин за навигация е чрез Link компонента. Работи подобно на <a> тага в уеб, така че ако идвате от уеб разработка, ще ви е познато:

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

export default function HomeScreen() {
  return (
    
      Добре дошли

      {/* Прост линк */}
      
        За нас
      

      {/* Линк с динамичен параметър */}
      
        Статия #42
      

      {/* Линк, който замества текущия екран в стека */}
      
        Настройки
      

      {/* Линк като бутон */}
      
        
          Моят профил
        
      
    
  );
}

const styles = StyleSheet.create({
  container: { flex: 1, padding: 16, justifyContent: 'center' },
  title: { fontSize: 24, fontWeight: 'bold', marginBottom: 20 },
  link: { color: '#007AFF', fontSize: 16, marginVertical: 8 },
  button: { backgroundColor: '#007AFF', padding: 12, borderRadius: 8 },
  buttonText: { color: 'white', textAlign: 'center', fontWeight: '600' },
});

Програмна навигация с useRouter

За навигация от код (например след успешно изпращане на форма) ще използвате хука useRouter:

import { useRouter } from 'expo-router';
import { View, Button, Alert } from 'react-native';

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

  const handleLogin = async () => {
    try {
      await authenticateUser();

      // Навигиране и замяна на текущия екран
      router.replace('/(tabs)');
    } catch (error) {
      Alert.alert('Грешка', 'Неуспешно влизане');
    }
  };

  const handleGoBack = () => {
    if (router.canGoBack()) {
      router.back();
    } else {
      router.replace('/');
    }
  };

  return (
    
      

Разлики между push, replace и navigate

Expo Router предлага три основни метода за навигация и разликата между тях е важна. Не ги бъркайте:

  • router.push(href) — Добавя нов екран в стека. Потребителят може да се върне назад.
  • router.replace(href) — Замества текущия екран. Няма връщане назад към заменения.
  • router.navigate(href) — Интелигентна навигация. Ако маршрутът вече е в стека, връща се до него вместо да създава дубликат.
// Пример за разликите
const router = useRouter();

// Добавя /details в стека (Home → Details)
router.push('/details');

// Замества текущия екран (Home се замества с Details)
router.replace('/details');

// Ако Details вече е в стека, връща се до него
// Ако не е — добавя го като push
router.navigate('/details');

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

Единичен динамичен сегмент

Файлове с квадратни скоби в името дефинират динамични маршрути. Ако идвате от Next.js, веднага ще се ориентирате:

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

interface BlogPost {
  id: string;
  title: string;
  content: string;
  author: string;
  publishedAt: string;
}

export default function BlogPostScreen() {
  const { id } = useLocalSearchParams<{ id: string }>();
  const [post, setPost] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetchPost(id).then(data => {
      setPost(data);
      setLoading(false);
    });
  }, [id]);

  if (loading) return ;
  if (!post) return Статията не е намерена;

  return (
    
      {post.title}
      
        от {post.author} • {post.publishedAt}
      
      {post.content}
    
  );
}

const styles = StyleSheet.create({
  container: { flex: 1, padding: 16 },
  title: { fontSize: 24, fontWeight: 'bold', marginBottom: 8 },
  meta: { color: '#666', marginBottom: 16 },
  content: { fontSize: 16, lineHeight: 24 },
});

Множество динамични параметри

Можете да комбинирате статични и динамични сегменти без проблем:

app/
└── shop/
    └── [category]/
        └── [productId].tsx    → /shop/electronics/42
// app/shop/[category]/[productId].tsx
import { useLocalSearchParams } from 'expo-router';

export default function ProductScreen() {
  const { category, productId } = useLocalSearchParams<{
    category: string;
    productId: string;
  }>();

  return (
    
      Категория: {category}
      Продукт: {productId}
    
  );
}

Catch-all маршрути

За маршрути, които трябва да хващат произволен брой сегменти, използвайте тройни точки:

// app/docs/[...path].tsx — съвпада с /docs/a, /docs/a/b, /docs/a/b/c
import { useLocalSearchParams } from 'expo-router';

export default function DocsScreen() {
  const { path } = useLocalSearchParams<{ path: string[] }>();

  // /docs/react/hooks/useState → path = ['react', 'hooks', 'useState']
  const fullPath = Array.isArray(path) ? path.join('/') : path;

  return Документация: {fullPath};
}

Групи за организация — скоби без промяна на URL

Скобите () в имената на директории създават логически групи без да променят URL пътищата. Лично аз смятам, че това е една от най-полезните функции — особено за разделяне на автентикирани и неавтентикирани потоци:

app/
├── _layout.tsx
├── (auth)/
│   ├── _layout.tsx
│   ├── login.tsx          → /login
│   └── register.tsx       → /register
├── (app)/
│   ├── _layout.tsx
│   ├── (tabs)/
│   │   ├── _layout.tsx
│   │   ├── index.tsx      → /
│   │   ├── explore.tsx    → /explore
│   │   └── profile.tsx    → /profile
│   └── settings.tsx       → /settings
└── +not-found.tsx

Забележете — (auth) и (app) не се появяват в URL адресите. URL-ът за login е просто /login, не /auth/login. Чисто и подредено.

Аутентикация и защитени маршрути

Имплементация на auth контекст

Expo Router работи чудесно с React Context за управление на аутентикацията. Ето една пълна имплементация, която можете да адаптирате за вашия проект:

// contexts/AuthContext.tsx
import { createContext, useContext, useState, useEffect, ReactNode } from 'react';
import * as SecureStore from 'expo-secure-store';

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

interface AuthContextType {
  user: User | null;
  isLoading: boolean;
  signIn: (email: string, password: string) => Promise<void>;
  signOut: () => Promise<void>;
  signUp: (name: string, email: string, password: string) => Promise<void>;
}

const AuthContext = createContext(null);

export function AuthProvider({ children }: { children: ReactNode }) {
  const [user, setUser] = useState(null);
  const [isLoading, setIsLoading] = useState(true);

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

  const loadStoredAuth = async () => {
    try {
      const token = await SecureStore.getItemAsync('auth_token');
      if (token) {
        const userData = await fetchUserProfile(token);
        setUser(userData);
      }
    } catch (error) {
      console.error('Грешка при зареждане на сесията:', error);
    } finally {
      setIsLoading(false);
    }
  };

  const signIn = async (email: string, password: string) => {
    const response = await api.post('/auth/login', { email, password });
    await SecureStore.setItemAsync('auth_token', response.token);
    setUser(response.user);
  };

  const signOut = async () => {
    await SecureStore.deleteItemAsync('auth_token');
    setUser(null);
  };

  const signUp = async (name: string, email: string, password: string) => {
    const response = await api.post('/auth/register', { name, email, password });
    await SecureStore.setItemAsync('auth_token', response.token);
    setUser(response.user);
  };

  return (
    
      {children}
    
  );
}

export const useAuth = () => {
  const context = useContext(AuthContext);
  if (!context) throw new Error('useAuth трябва да се използва в AuthProvider');
  return context;
};

Пренасочване въз основа на аутентикация

В root layout-а използвайте Redirect компонента за автоматично пренасочване. Тази част е наистина готина — потребителят просто бива „прехвърлен" към правилния екран:

// app/_layout.tsx
import { Stack, Redirect } from 'expo-router';
import { AuthProvider, useAuth } from '../contexts/AuthContext';
import { ActivityIndicator, View } from 'react-native';

function RootLayoutNav() {
  const { user, isLoading } = useAuth();

  if (isLoading) {
    return (
      
        
      
    );
  }

  return (
    
      {user ? (
        
      ) : (
        
      )}
      
    
  );
}

export default function RootLayout() {
  return (
    
      
    
  );
}

С тази имплементация потребителят автоматично се пренасочва към екрана за вход, ако не е аутентикиран. След успешно влизане навигацията се превключва към основното приложение — без ръчна навигация, без допълнителен код.

Типизирани маршрути с TypeScript

Автоматично генериране на типове

Expo Router може автоматично да генерира TypeScript типове за всички маршрути, което е страхотно за предотвратяване на грешки. Активирайте го в app.json:

{
  "expo": {
    "experiments": {
      "typedRoutes": true
    }
  }
}

След стартиране на npx expo start, рутерът генерира файл .expo/types/router.d.ts с типове за всички маршрути. Резултатът? Autocomplete и компилационна проверка:

import { Link, useRouter } from 'expo-router';

// TypeScript ще покаже грешка, ако маршрутът не съществува
Профил           // ✓ Валиден
Нищо          // ✗ Грешка!

const router = useRouter();
router.push('/blog/42');                       // ✓ Валиден
router.push('/invalid-route');                  // ✗ Грешка!

Типове за динамични параметри

С useLocalSearchParams можете да типизирате параметрите лесно:

// Генерираните типове автоматично знаят за [id] параметъра
import { useLocalSearchParams } from 'expo-router';

export default function PostScreen() {
  // TypeScript знае, че id е string
  const { id } = useLocalSearchParams<{ id: string }>();

  // Допълнителни query параметри
  const { id, sort, page } = useLocalSearchParams<{
    id: string;
    sort?: string;
    page?: string;
  }>();
}

Deep Linking — безпроблемни връзки към приложението

Автоматична настройка

Едно от най-големите предимства на Expo Router е, че deep linking работи практически автоматично. Тъй като маршрутите се базират на файловата система, URL адресите директно съвпадат с екраните. Няма нужда от допълнителни mapping-и.

Конфигурирайте URL схемата в app.json:

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

Universal Links (iOS) и App Links (Android)

С правилната конфигурация URL адресите от вашия уебсайт автоматично отварят съответните екрани:

  • https://myapp.com/blog/42 → отваря app/blog/[id].tsx с id=42
  • https://myapp.com/profile → отваря app/profile/index.tsx
  • myapp://settings → отваря app/settings.tsx

Не е необходима допълнителна конфигурация на маршрутите за deep linking. Expo Router разбира структурата автоматично и просто работи.

Модални прозорци и споделени елементи

Модали

Модалните прозорци се конфигурират чрез опцията presentation в layout-а. Ето два варианта — стандартен модал и прозрачен:

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

export default function RootLayout() {
  return (
    
      
      
      
    
  );
}
// app/modal.tsx
import { useRouter } from 'expo-router';
import { View, Text, Button, StyleSheet } from 'react-native';

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

  return (
    
      Създаване на нов пост
      {/* Съдържание на формата */}
      

Персонализирана 404 страница и обработка на грешки

Not Found екран

Expo Router предлага готова конвенция за обработка на несъществуващи маршрути. Просто създайте файл +not-found.tsx:

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

export default function NotFoundScreen() {
  return (
    <>
      
      
        🔍
        Упс! Тази страница не съществува
        
          Възможно е връзката да е грешна или страницата да е преместена
        
        
          Обратно към началната страница
        
      
    
  );
}

const styles = StyleSheet.create({
  container: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: 20 },
  emoji: { fontSize: 64, marginBottom: 16 },
  title: { fontSize: 22, fontWeight: 'bold', marginBottom: 8, textAlign: 'center' },
  subtitle: { fontSize: 16, color: '#666', textAlign: 'center', marginBottom: 20 },
  link: { color: '#007AFF', fontSize: 16, fontWeight: '600' },
});

Error Boundary

Всеки маршрут може да експортира ErrorBoundary компонент, което е много полезно за грациозна обработка на грешки без да се срине цялото приложение:

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

export function ErrorBoundary({ error, retry }: { error: Error; retry: () => void }) {
  return (
    
      Нещо се обърка
      {error.message}
      

Drawer навигация

За приложения със странична навигация (drawer / хамбургер меню), Expo Router поддържа и Drawer layout. Първо инсталирайте зависимостите:

npx expo install @react-navigation/drawer react-native-gesture-handler react-native-reanimated
// app/(drawer)/_layout.tsx
import { Drawer } from 'expo-router/drawer';
import { Ionicons } from '@expo/vector-icons';

export default function DrawerLayout() {
  return (
    
       (
            
          ),
        }}
      />
       (
            
          ),
        }}
      />
       (
            
          ),
        }}
      />
    
  );
}

Съвети за производителност и добри практики

Lazy Loading на екрани

По подразбиране Expo Router лениво зарежда екраните — те се импортират едва когато потребителят навигира до тях. Но можете да оптимизирате допълнително:

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

export default function TabLayout() {
  return (
    
      {/* ... */}
    
  );
}

Оптимизиране на списъци с маршрути

При екрани със списъци горещо препоръчвам FlashList от Shopify вместо стандартния FlatList. Разликата в производителността е забележима:

import { Link } from 'expo-router';
import { FlashList } from '@shopify/flash-list';
import { Pressable, Text, View } from 'react-native';

export default function BlogListScreen() {
  const posts = usePosts();

  return (
     (
        
          
            {item.title}
            {item.excerpt}
          
        
      )}
    />
  );
}

Предзареждане на данни

Можете да предзареждате данни за екран, преди потребителят да навигира до него. Това е малка оптимизация, но прави навигацията значително по-гладка:

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

export default function HomeScreen() {
  // Предзарежда маршрута при рендериране на компонента
  usePrefetchRoute('/profile');

  return (
    
      
        Виж профила
      
    
  );
}

Миграция от React Navigation към Expo Router

Ако вече имате проект с React Navigation, миграцията е по-лесна, отколкото си мислите. Сериозно. Ето основните стъпки:

  1. Създайте директорията app/ — преместете екраните от навигационната конфигурация в съответните файлове.
  2. Конвертирайте навигаторите в layoutscreateStackNavigator() става Stack в _layout.tsx, createBottomTabNavigator() става Tabs.
  3. Заменете navigation.navigate() с router.push() или Link.
  4. Заменете navigation.getParam() с useLocalSearchParams().
  5. Премахнете ръчната deep linking конфигурация — Expo Router я управлява автоматично.
// ПРЕДИ: React Navigation
const Stack = createStackNavigator();
function App() {
  return (
    
      
        
        
      
    
  );
}

// СЛЕД: Expo Router
// app/_layout.tsx
import { Stack } from 'expo-router';
export default function Layout() {
  return ;
}

// app/index.tsx — HomeScreen
// app/details.tsx — DetailsScreen

Често задавани въпроси

Мога ли да използвам Expo Router без Expo?

Expo Router е проектиран да работи в рамките на Expo екосистемата и зависи от Metro bundler-а с Expo конфигурация. Ако използвате чист React Native проект (bare workflow), можете да добавите Expo чрез npx install-expo-modules и след това да инсталирате рутера. Технически обаче той изисква Expo инфраструктура, така че напълно без Expo — не, няма да стане.

Как да скрия определен маршрут от Tab навигацията?

Използвайте href: null в опциите на екрана. Маршрутът остава достъпен чрез програмна навигация, но просто не се показва в tab bar-а:

Expo Router поддържа ли Server-Side Rendering (SSR)?

Да, поддържа. Expo Router v4 има поддръжка за SSR и статично генериране за уеб платформата. Конфигурирате уеб изхода като "server" или "static" в app.json. За мобилните платформи SSR не е приложимо, но уеб версията на приложението ви получава SEO предимства.

Как работи Expo Router с EAS Update?

Expo Router е напълно съвместим с EAS Update (over-the-air ъпдейти). Добавянето на нови маршрути или промяната на съществуващи се поддържа чрез OTA ъпдейти без проблем. Единственото ограничение — промени в app.json (като deep linking схемата) изискват ново изграждане на приложението.

Каква е разликата между useLocalSearchParams и useGlobalSearchParams?

useLocalSearchParams връща параметрите, специфични за текущия маршрут, и не се актуализира при навигация до друг екран. useGlobalSearchParams връща глобалните URL параметри и се актуализира при всяка промяна. В повечето случаи useLocalSearchParams е правилният избор — предотвратява ненужни повторни рендирания и е по-предвидим.

За Автора Editorial Team

Our team of expert writers and editors.