React Native

Основи Expo Router

File-based routing у Expo — тека app/, layout, Stack і Tabs, Link і useRouter, назад, typed routes; довідник міст і навігаційна оболонка Nomad

Основи Expo Router

З чого почати

До цього моменту майже весь Nomad жив на одному «головному» екрані: список поїздок, місця в шапці списку, кнопка «Нова поїздка». Форму створення ми вже винесли в окремий файл create-trip.tsx і відкривали через router.push — це був мінімальний натяк на навігацію, без пояснення, чому файл стає екраном і як влаштована оболонка всього застосунку.

У реальному мобільному продукті екранів десятки: стрічка, деталі, налаштування, пошук, вхід. Користувач очікує:

  • нижні вкладки для головних розділів (як у Instagram чи банківському клієнті);
  • стопку екранів зі стрічкою «назад» (відкрили деталі — повернулись жестом або кнопкою);
  • передбачувану поведінку системної кнопки «назад» на Android.

Expo Router — бібліотека навігації в екосистемі Expo, яка будує ці екрани з файлів і тек у каталозі app/. Ідея близька до file-based routing у Next.js: шлях на диску визначає маршрут (route) у застосунку.

Після статті ви зможете:

  1. Пояснити, що таке file-based routing і чим app/ відрізняється від src/.
  2. Зібрати _layout.tsx як оболонку (providers + навігатор).
  3. Налаштувати Tabs (нижні вкладки) і Stack (стопка).
  4. Переходити між екранами через Link і useRouter (push, back, replace).
  5. Зрозуміти групи (tabs), index-маршрути, заголовок і кнопку «назад».
  6. Увімкнути typed routes на рівні знайомства.
  7. Зібрати міні-проєкт «Довідник міст» і перебудувати Nomad на tabs + create-trip.
Головна думка. Екран у Expo Router — це не «просто компонент у switch». Це файл у app/, який бібліотека перетворює на маршрут. Layout — постійна оболонка навколо дочірніх маршрутів. Tabs і Stack — два різні способи змінювати, який дочірній екран видно зараз.
Прев’ю на сайті.::react-native-preview не містить Expo Router. Демо навігації перевіряйте в Expo Go / симуляторі. На сторінці — схеми, код і текстові розбори.

Код Nomad: github.com/arakviel/nomad.


Навіщо навігація окремою темою

У веб-React ви звикли до «сторінок» і URL. На телефоні немає адресного рядка в тому ж вигляді, але задача та сама: показати інший екран, запам’ятати, звідки прийшли, дати повернутись.

Без бібліотеки навігації новачки часто роблять:

const [screen, setScreen] = useState<'home' | 'form'>('home');
if (screen === 'form') return <Form onBack={() => setScreen('home')} />;
return <Home onOpenForm={() => setScreen('form')} />;

Для двох екранів це працює. Далі з’являються:

  • вкладені «стопки» (home → detail → edit);
  • вкладки, які не повинні знищувати стан сусідньої вкладки;
  • системний жест «назад»;
  • deep link «відкрий одразу цей екран» (наступні статті).

React Navigation — низькорівнева бібліотека навігації для React Native (стек, таби, drawer). Expo Router будується поверх React Navigation і додає file-based шар: ви менше пишете конфіг «список екранів у JS-об’єкті», більше — структуру тек.

У курсі основний шлях — Expo Router, бо Nomad уже на Expo, і той самий підхід узгоджується з документацією Expo.


Місток з web React

На вебіУ Expo RouterКоментар
URL /aboutмаршрут /about з файлу app/about.tsxшлях «схожий» на URL
React Router / Next.js app/Expo Router app/file-based routing
<a href> / <Link><Link href="…"> з expo-routerдекларативний перехід
router.push (Next)router.push з useRouter()імперативний перехід
layout.tsx (Next)_layout.tsxоболонка сегмента
вкладені layoutвкладені _layout у підтекахTabs всередині Stack тощо
query/params у URLparams (глибше — стаття 13)на старті — статичні шляхи
Не плутати.react-router-dom з вебу не ставлять у React Native для цієї задачі. API інший, навігатори — нативні. Expo Router — окремий пакет (expo-router), уже є в залежностях Nomad.

Чому «файл = маршрут» зручно

У Next.js звичка: створив app/settings/page.tsx — з’явилась сторінка settings. У Expo Router: створив app/settings.tsx (або app/settings/index.tsx) — з’явився екран, на який можна href="/settings".

Переваги для команди:

  • структура проєкту читається як карта застосунку;
  • менше «магічного» реєстру екранів у одному великому файлі;
  • deep linking (пізніше) природно лягає на ті самі шляхи.

Обмеження, про які варто знати одразу:

  • не кожен файл у app/ — екран (наприклад _layout.tsx — оболонка);
  • імена з _ і () мають спеціальний сенс;
  • навігація — не лише файли: треба обрати Stack vs Tabs і налаштувати заголовки.


Тека app/ і src/

У структурі курсу (стаття 05):

  • app/маршрути й layout навігації (те, що «є екраном» для користувача).
  • src/логіка, UI-компоненти, тема, features (те, що екрани імпортують).

Екран має лишатися тонким: зібрав дані з хуків, розклав Screen / списки / форми з @/features і @/shared. Бізнес-логіку в app/ не розмножують без потреби — інакше маршрути перетворюються на «божественні» файли на тисячі рядків.

У Nomad після цієї статті:

app/
  _layout.tsx           ← корінь: providers + Stack
  (tabs)/
    _layout.tsx         ← Tabs
    index.tsx           ← /  (Поїздки)
    places.tsx          ← /places
    about.tsx           ← /about
  create-trip.tsx       ← /create-trip (Stack поверх tabs)
src/
  features/trips/…
  shared/…

Що таке маршрут (route) і екран (screen)

Маршрут (route) — адреса в системі навігації, наприклад /places або /create-trip. Це логічне ім’я «куди можна перейти».

Екран (screen) — React-компонент, який малюється, коли цей маршрут активний. У file-based підході екран майже завжди = default export з файлу в app/.

Користувач не бачить рядок /places у браузерному сенсі (хоча на web-збірці Expo він може з’явитись у URL). Він бачить вміст і, за наявності, вкладки / заголовок / кнопку назад.


_layout.tsx — оболонка сегмента

Файл _layout.tsx у теці app (або вкладеній теці) не є окремим «екраном у меню». Це layout (розкладка / оболонка): компонент, який обгортає всі маршрути цієї теки.

Типові ролі кореневого layout:

  1. Підключити провайдери (ThemeProvider, TripsProvider, пізніше Redux).
  2. Задати кореневий навігатор (Stack або інший).
  3. Налаштувати StatusBar, спільний фон.

Дочірні екрани вставляються всередину навігатора. Коли ви переходите з «Поїздки» на «Місця», providers не розмонтовуються — стейт теми й список trips живе далі.

_layout.tsx
файл-оболонка
Один layout на теку app або підтеку. Ім’я з підкресленням на початку — домовленість Expo Router: «це не route page, а layout». Усередині зазвичай return <Stack /> або <Tabs />, або вкладений layout.
Slot
компонент (рідше на старті)
«Дірка», куди Router вставляє дочірній маршрут, якщо ви не використовуєте Stack/Tabs, а будуєте власну оболонку. На старті курсу достатньо Stack і Tabs — вони самі керують «діркою».
screenOptions
об’єкт / функція
Спільні налаштування всіх екранів навігатора: чи показувати header, кольори, анімація. Окремий екран можна перевизначити через <Stack.Screen name="…" options={…} />.

Приклад ідеї кореневого layout (схема)

export default function RootLayout() {
  return (
    <ThemeProvider>
      <TripsProvider>
        <Stack>
          <Stack.Screen name="(tabs)" options={{ headerShown: false }} />
          <Stack.Screen name="create-trip" options={{ title: 'Нова поїздка' }} />
        </Stack>
      </TripsProvider>
    </ThemeProvider>
  );
}

Провайдери зовні Stack — щоб і вкладки, і create-trip бачили той самий useTrips() і useTheme().


index і імена файлів

ФайлТиповий шлях
app/index.tsx/ (стартовий)
app/places.tsx/places
app/create-trip.tsx/create-trip
app/(tabs)/index.tsx/ якщо (tabs) — частина кореня без сегмента в URL

index означає «маршрут самої теки». У групі (tabs) файл index.tsx — домашня вкладка з шляхом /.

Імена файлів краще kebab-case (create-trip.tsx) — вони стають сегментами URL. Уникайте пробілів і кирилиці в іменах файлів маршрутів.


Stack — стопка екранів

Stack (стек, стопка) — навігатор, де екрани лягають один на одного. Новий екран штовхають (push) на вершину; «назад» знімає вершину (back / pop).

Аналогія: стопка аркушів. Верхній видно повністю; під ним лежать попередні. Коли прибираєте верхній — знову бачите той, що був під ним.

Типові сценарії Stack:

  • список → деталі;
  • головна → форма створення;
  • налаштування → підекран «про застосунок».

У React Navigation / Expo Router Stack за замовчуванням показує header (смуга зверху з заголовком і кнопкою назад) — на iOS і Android з різним виглядом, але зі спільною ідеєю.

Stack
компонент expo-router
Навігатор-стопка. import { Stack } from 'expo-router'. Дочірні файли теки автоматично стають екранами цього стека (якщо layout повертає <Stack />).
Stack.Screen
конфіг екрана
Дозволяє задати name (ім’я файлу/сегмента без розширення) і options: title, headerShown, presentation, headerBackTitle тощо. Не замінює файл екрана — лише налаштовує його в навігаторі.
presentation
string (опції платформи)
Як показувати екран: звичайна картка (card), модальне вікно (глибше в статті 12) тощо. Для create-trip у Nomad достатньо звичайного card поверх tabs.
headerShown
boolean
Чи показувати системну смугу заголовка. Для вкладок часто false (свій UI в контенті); для форми — true, щоб отримати стандартну «Назад».
headerBackTitle
string (iOS)
Текст поруч зі стрілкою «назад» на iOS (наприклад «Назад»). На Android частіше лише іконка/системна поведінка.

Системна кнопка «назад»

  • iOS: свайп від лівого краю + кнопка в header.
  • Android: системна кнопка/жест «Back» зазвичай попає стек, якщо навігатор налаштований правильно.

router.back() у коді робить те саме програмно: повернутись на попередній екран у історії. Якщо історії немає (відкрили deep link одразу на форму) — поведінка залежить від конфігурації; для базового сценарію «відкрили з tabs» back повертає на вкладки.


Tabs — нижні вкладки

Tabs (вкладки) — навігатор, де кілька рівноправних розділів доступні з панелі (зазвичай знизу на iOS/Android). Перемикання не будує глибоку стопку «вкладка1 → вкладка2 → вкладка3» у тому ж сенсі, що Stack: це паралельні корені розділів.

Користувач очікує:

  • іконку + короткий підпис;
  • активний стан іншим кольором;
  • збереження позиції скролу / стану при поверненні на вкладку (часто так і є, якщо екран не розмонтували — деталі залежать від налаштувань lazy / unmount).
Tabs
компонент expo-router
import { Tabs } from 'expo-router'. Layout у теці з вкладками зазвичай return <Tabs>…</Tabs>.
Tabs.Screen
конфіг вкладки
name = ім’я файлу (index, places, about). options.title — підпис на панелі. tabBarIcon — функція ({ color, size }) => <Icon … />.
tabBarActiveTintColor / tabBarInactiveTintColor
color
Колір активної / неактивної іконки й підпису. Узгоджуйте з темою (colors.primary / textSecondary).
tabBarStyle
ViewStyle
Стиль самої панелі: фон, верхня межа. У dark theme без цього панель може лишитись «світлою» посеред темного контенту.
headerShown: false
у screenOptions Tabs
Часто ховають header Tabs, бо кожна вкладка малює свій заголовок у контенті (AppText variant="title"), як у Nomad.

Іконки

У прикладі Nomad — @expo/vector-icons (Ionicons). Пакет ставлять через npx expo install @expo/vector-icons (версія під SDK). Можна обійтись без іконок (лише текст), але продуктовий вигляд зазвичай з іконками.


Групи (name) — дужки в імені теки

Тека (tabs)група (group). Круглі дужки в імені означають: ця тека організовує файли, але не додає сегмент у шлях URL.

На дискуШлях
app/(tabs)/index.tsx/
app/(tabs)/places.tsx/places
app/(tabs)/about.tsx/about

Без дужок app/tabs/places.tsx був би /tabs/places — зайвий сегмент у шляху.

Навіщо група:

  • зібрати вкладки в один layout Tabs;
  • відокремити «зона авторизації» (auth) (стаття 12);
  • тримати корінь Stack чистим: (tabs) + модалки + create-trip.

Компонент Link з expo-router схожий на якір у вебі: у JSX видно, куди веде елемент.

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

<Link href="/create-trip">
  <Text>Нова поїздка</Text>
</Link>

Часто потрібен свій вигляд кнопки. Prop asChild каже: «не малюй свій wrapper, використай дитину як pressable»:

<Link href="/create-trip" asChild>
  <Pressable style={styles.btn}>
    <Text>Нова поїздка</Text>
  </Pressable>
</Link>

У Nomad на вкладці «Ще» є приклад Link + asChild + Pressable.

useRouter — імперативний перехід

Хук useRouter() повертає об’єкт з методами навігації. Зручно всередині колбеків: після успішного submit, після логіну, у onPress кнопки, коли шлях обчислюється динамічно.

router.push(href)
метод
Відкрити маршрут, додавши його в історію Stack. З tabs на /create-trip — типовий push: з’явиться «назад».
router.replace(href)
метод
Відкрити маршрут, замінивши поточний запис історії. Корисно після логіну («не повертатись на екран пароля кнопкою назад»). На старті курсу рідше, ніж push.
router.back()
метод
Повернутись на попередній екран у історії. Після створення поїздки в Nomad — router.back(), щоб знову побачити tabs і оновлений список.
router.canGoBack()
метод → boolean
Чи є куди повертатись. Корисно, щоб не викликати back «в порожнечу» (рідкісні cold-start сценарії).
import { useRouter } from 'expo-router';

const router = useRouter();

// кнопка
router.push('/create-trip');

// після збереження форми
router.back();

Коли що обирати

СитуаціяЗасіб
Пункт меню, картка «відкрити»Link (видно в розмітці)
Після submit / умови в кодіrouter.push / back
Потрібен повний контроль над PressableLink + asChild або лише router у onPress

href і шляхи

href — рядок (або об’єкт) адреси маршруту. На старті курсу — статичні шляхи:

  • "/" — домашня вкладка;
  • "/places";
  • "/about";
  • "/create-trip".

Динамічні сегменти ([id]) і params — стаття 13. Тут свідомо не ускладнюємо: спочатку стабільна карта статичних екранів.

Шлях має існувати як файл у app/. Друкарська помилка "/crate-trip" не знайде екран. Typed routes (нижче) допомагають TypeScript спіймати частину таких помилок.

Typed routes (коротко)

Typed routes — експеримент / опція Expo Router, коли TypeScript знає допустимі href з вашої теки app/.

У app.json:

"experiments": {
  "typedRoutes": true
}

Після цього Link і router.push суворіше перевіряють рядки шляхів. Іноді потрібен перезапуск TS-сервера / npx expo start. Це не замінює тести навігації, але зменшує опечатки.

У Nomad опцію вмикаємо в цій статті на рівні «підтримка з боку проєкту».


Header, safe area і вкладки

Коли headerShown: true на Stack-екрані, системний header займає місце зверху. Ваш Screen із SafeAreaView може додати зайвий відступ, якщо header уже врахував safe area — візуально «подвійний» зазор.

У create-trip Nomad після цієї статті:

  • заголовок і «назад» — від Stack;
  • контент — CreateTripForm без власного рядка «← Назад» (прибрали дубль).

На вкладках header сховано — safe area лишається на Screen.


Антипатерни

1. Усе через useState('screen') у великому App

Швидкий прототип — так; продукт із вкладками, жестом назад і deep link — ні. Ви винаходите те, що Router уже розв’язав.

2. Бізнес-логіка лише в app/

Маршрути товстіють, features не перевикористовуються. Тримайте тонкі екрани + src/features.

3. Плутати групу (tabs) з сегментом URL

Очікувати URL /tabs/places і дивуватись, що працює /places.

4. push замість back після форми

push('/') після створення поїздки може накласти ще один home у стек; «назад» тоді поверне на порожню форму. Після модального/форменного сценарію частіше back() або replace.

5. Забути theme на tab bar

Контент темний, панель вкладок біла «з коробки» — треба tabBarStyle і tint з теми.

6. Ховати create-trip всередині tabs без потреби

Форма як п’ята вкладка — дивний UX. Краще Stack-екран поверх tabs (як у Nomad).


Міні-проєкт: «Довідник міст»

Мета

Окремий застосунок поза Nomad. Шлях від А до Я: команди, структура тек, повний код кожного файлу. Можна відтворити без домислів.

Сюжет:

  1. Tabs: Список | Про нас.
  2. Список міст → тап → Stack-екран деталей із системною «назад».
  3. «Про нас» — текст + Link на список.

Кінцева структура

city-guide/
  package.json
  app.json
  app/
    _layout.tsx
    (tabs)/
      _layout.tsx
      index.tsx
      about.tsx
    city/
      [id].tsx
  src/
    data/
      cities.ts

Крок 1. Створити проєкт

1. Шаблон з tabs і Router

npx create-expo-app@latest city-guide -t tabs
cd city-guide

У package.json має бути "main": "expo-router/entry" (у шаблоні вже так).

2. Іконки (якщо TypeScript не знаходить пакет)

npx expo install @expo/vector-icons

3. Після підстановки всіх файлів

npx expo start

Крок 2. Дані — src/data/cities.ts

Створіть теки src/data/ і файл цілком:

Крок 3. Корінь Stack — app/_layout.tsx

Повністю замініть вміст:

Крок 4. Tabs — app/(tabs)/_layout.tsx

Повністю замініть вміст. Лише дві вкладки: index і about.

Крок 5. Список — app/(tabs)/index.tsx

Повністю замініть вміст:

Крок 6. «Про нас» — app/(tabs)/about.tsx

Повністю замініть (або створіть, якщо не було):

Крок 7. Деталі — app/city/[id].tsx

Створіть теку app/city/ і файл [id].tsx цілком:

useLocalSearchParams читає id з шляху /city/kyiv. Глибше про params і deep links — у статті 13; тут мінімум, щоб Stack мав сенс.

Крок 8. Прибрати зайве з шаблону tabs

  1. Видаліть app/(tabs)/explore.tsx (або інший другий tab шаблону), якщо він є.
  2. У (tabs)/_layout.tsx мають лишитись лише index і about (як у нашому файлі).
  3. Видаліть невикористані компоненти/ассети шаблону за бажанням.

Опційно в app.json всередині expo:

"experiments": {
  "typedRoutes": true
}

Крок 9. Перевірка

1. Старт

npx expo start → Expo Go / симулятор.

2. Вкладки

Внизу «Список» і «Про нас»; перемикання змінює екран.

3. Деталі

Тап «Львів» → опис; header зі стрілкою / «Назад»; жест назад на iOS/Android.

4. Про нас

Текст і Link «До списку міст».

5. Невідомий id

Якщо відкрити /city/xxx (через Link у коді тимчасово) — «Місто не знайдено».

Критерій «готово»

  • create-expo-app -t tabs
  • Усі файли кроків 2–7 з повним кодом (не фрагменти «допишіть»)
  • Дві вкладки українською
  • Список → деталі → назад
  • Немає useState('screen') замість Router
  • Дані в src/data/cities.ts

Карта ідей курсу → файли

ІдеяДе
File-based routingapp/**
Tabs(tabs)/_layout.tsx
Stack + backкореневий _layout + city/[id]
Linkсписок, about
Група (tabs)не в URL
Динамічний сегмент[id] + useLocalSearchParams

Nomad: file-based navigation structure

Навіщо користувачу

Застосунок перестає бути «одним довгим екраном». З’являються розділи:

  • Поїздки — стрічка, тема, sticky «Нова поїздка» (як раніше);
  • Місця — усі mock-місця сіткою;
  • Ще — опис, тема, приклад Link на форму.

Форма створення — окремий stack-екран із системним заголовком і «назад», без дубльованого UI «← Назад» у контенті.

Нитка проєкту

Уже є (і зберігаємо):

  • ThemeProvider, чіпи теми (на вкладці Поїздки; також на «Ще»);
  • FlashList поїздок, pull-to-refresh, TripCard, місця в header списку;
  • CreateTripForm (RHF+Zod, усі контроли);
  • TripsProvider, addTrip, картки з мітками.

Додаємо / змінюємо:

ЗмінаНавіщо
app/(tabs)/ + Tabs layoutтри головні розділи
перенесення home → (tabs)/index.tsxмаршрут / у вкладках
places.tsx, about.tsxнові вкладки
кореневий Stack з options для create-tripheader + back
typedRoutes: trueпідтримка типізації шляхів
@expo/vector-iconsіконки tab bar
спрощення create-trip.tsxбез свого back-рядка

Встановлення іконок (якщо збираєте вручну)

cd /path/to/nomad
npx expo install @expo/vector-icons
# за peer-конфліктів npm: npm install @expo/vector-icons --legacy-peer-deps

Повний знімок проєкту

Перевірка

  1. Внизу три вкладки: Поїздки, Місця, Ще (іконки + підписи).
  2. На «Поїздки» — стрічка, тема, місця в header, sticky «Нова поїздка».
  3. «Нова поїздка» → екран із системним заголовком і назад; форма як раніше.
  4. Успішне створення → back на вкладки, картка зверху списку.
  5. «Місця» — сітка всіх mock-місць.
  6. «Ще» — текст + тема + Link на форму.
  7. Перемикання вкладок не губить тему (провайдер на корені).

Коміт

cd /path/to/nomad
git add -A
git commit -m "$(cat <<'EOF'
feat: file-based navigation structure

Material: content/15.react-native/11.expo-router-basics.md
EOF
)"
git push

Прев’ю (обмежене)

У iframe Router немає. Нижче — макет трьох «вкладок» на state (ідея UX). Повна поведінка — у Expo Go.

TSXNomadTabsMock.tsx
iPhone
9:41

Loading…

react-native-web · not a real device


Практичні завдання

Базовий рівень

  1. Своїми словами: чим Stack відрізняється від Tabs для користувача.
  2. Пояснити, чому (tabs) не потрапляє в URL.
  3. Назвати різницю між Link і router.push.

Середній рівень

  1. Міні-проєкт «Довідник міст».
  2. У Nomad додати четверту вкладку «Чернетки»-заглушку (екран з текстом) і прибрати її з tab bar через href: null (див. docs) — або просто три вкладки без заглушки.
  3. Замінити один router.push на Link + asChild на кнопці «Нова поїздка».

Професійний рівень

  1. Увімкнути typed routes і навмисно зламати href — показати помилку TS.
  2. Порівняти push vs replace після create-trip (що з кнопкою «назад»).
  3. Нотатка: що станеться, якщо TripsProvider покласти всередину лише (tabs) layout, а не root — і чому форма тоді «не бачить» trips.

Часті запитання


Що далі

Базова карта маршрутів готова: tabs + stack. Далі — вкладені навігатори та модалки: Stack усередині вкладки, presentation: 'modal', групи auth, guard на back. Потім — params і deep links.

Expo Router · File-based routing · Tabs · Typed routes · Nomad

Copyright © 2026