Version2

Основи Expo Router

Архітектура File-based routing у Expo — сегрегація app/ та src/, життєвий цикл макетів _layout, навігатори Stack і Tabs, декларативна та імперативна маршрутизація через Link та useRouter, механіка повернення назад, Typed Routes, міні-проєкт «Довідник міст» та навігаційна оболонка Nomad

Основи Expo Router

Вступ: Навігація в мобільних додатках та файловий роутинг (File-Based Routing)

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

Реальний мобільний продукт ніколи не обмежується одним екраном. У ньому завжди є десятки різних екранів: новинні стрічки, детальні картки, панелі налаштувань, пошук, авторизація та модальні діалоги. Взаємодія з користувачем на мобільних пристроях ґрунтується на зрозумілих та звичних патернах:

  1. Нижні навігаційні вкладки (Bottom Tabs): основний орієнтир застосунку для швидкого доступу до головних розділів (як у банківських додатках чи соцмережах).
  2. Ієрархічна стопка екранів (Navigation Stack): організовує переходи углиб контенту за принципом «від списку до деталей» зі збереженням історії переходів та можливістю повернення назад свайпом чи кнопкою «Назад».
  3. Коректна обробка системних жестів та кнопок: підтримка апаратної кнопки повернення на Android та жестової смуги навігації на iOS.

Expo Router — це спеціалізований фреймворк маршрутизації та навігації для екосистеми Expo та React Native, який використовує файловий роутинг (File-Based Routing). Головна ідея запозичена з сучасних веб-фреймворків (зокрема Next.js App Router): структура файлів і папок усередині каталогу app/ автоматично перетворюється на навігаційні маршрути (Routes) та контейнери застосунку.

Навчальні цілі розділу:

  • Опанувати концепцію File-Based Routing у нативному мобільному середовищі та засвоїти правила архітектурного розмежування між каталогом маршрутів app/ та шаром бізнес-логіки й компонентів src/.
  • Дослідити архітектуру макетів _layout.tsx як постійної контекстної оболонки, що інкапсулює спільні провайдери станів (State Providers) та нативні навігатори.
  • Сконфігурувати базові навігаційні контейнери: паралельні вкладки Tabs та ієрархічну стопку Stack.
  • Засвоїти механізми переходами між маршрутами за допомогою декларативного компонента Link (з підтримкою властивості asChild) та імперативного хука useRouter (методи push, back, replace, canGoBack).
  • Зрозуміти семантику ізольованих груп маршрутів у круглих дужках (group), призначення файлів index.tsx, а також правила конфігурації нативного заголовка (Header) та безпечних зон (Safe Area Insets).
  • Активувати та інтегрувати механізм статичної типізації маршрутів Typed Routes для компіляційної верифікації шляхів навігації.
  • Розробити автономний навчальний міні-проєкт «Довідник міст» та здійснити рефакторинг проєкту Nomad на повноцінну навігаційну структуру з вкладками та модальною стопкою.
Головний принцип: Екран в Expo Router — це не просто випадковий компонент, який рендериться за умовою в switch або тернарному операторі. Це експортований за замовчуванням React-компонент у файлі всередині каталогу app/, який маршрутизатор перетворює на повноцінний вузол графа навігації. Файл _layout.tsx формує спільний каркас навколо дочірніх сегментів, тоді як навігатори Stack і Tabs визначають структуру та анімацію переходу між цими екранами.
Особливості тестування навігації. Інтерактивні прев'ю на веб-сайті (::react-native-preview) виконуються в ізольованому пісочничному оточенні без підключення нативного рушія Expo Router. Тому повноцінну перевірку переходів, стеків, жестів та збереження станів слід виконувати безпосередньо в середовищі Expo Go або нативному симуляторі/емуляторі. У тексті статті наведено вичерпні архітектурні схеми, код та аналітичні розбори.

Вихідний код репозиторію проєкту Nomad: github.com/arakviel/nomad.


Архітектурна необхідність виділеної системи навігації

У традиційній веб-розробці зміна представлення тісно пов'язана з адресним рядком браузера (URL), об'єктом window.history та подіями popstate. На мобільних пристроях адресний рядок відсутній, проте задачі залишаються тими самими: передбачувано відобразити потрібний екран, зберегти історію переходів та надати користувачеві можливість повернутися назад без втрати стану даних.

Початківці часто намагаються реалізувати перемикання екранів за допомогою локального реактивного стану:

const [currentScreen, setCurrentScreen] = useState<'home' | 'form'>('home');

if (currentScreen === 'form') {
  return <TripFormScreen onBack={() => setCurrentScreen('home')} />;
}

return <HomeScreen onOpenForm={() => setCurrentScreen('form')} />;

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

  1. Руйнування життєвого циклу компонентів і втрата стану: Умовний рендеринг через тернарний оператор або if повністю розмонтовує попередній компонент (HomeScreen). При поверненні назад дерево монтується заново, що призводить до втрати положення скролу у списках, скидання тимчасових фільтрів і повторного виконання мережевих запитів.
  2. Відсутність підтримки складних навігаційних ієрархій: Спроба вручну синхронізувати вкладені стопки переходів (наприклад, Home -> Details -> Edit -> Confirm), незалежні паралельні вкладки та модальні діалоги призводить до експоненційного ускладнення коду (State Explosion) та появи важковловимих багів.
  3. Повна втрата нативних анімацій та системних жестів: Ручне перемикання екранів не використовує апаратні транзиції операційної системи. Користувач втрачає плавний інерційний свайп повернення на iOS (Interactive Pop Gesture) та обробку системної кнопки «Назад» на Android.
  4. Неможливість обробки глибоких посилань (Deep Linking): Застосунок не здатний коректно запуститися за зовнішнім URL-посиланням (наприклад, nomad://trips/42 з push-повідомлення чи браузера), оскільки відсутній шар зіставлення рядкової адреси з вузлом дерева екранів.
       РУЧНЕ КЕРУВАННЯ СТАНОМ                          EXPO ROUTER (FILE-BASED)
+------------------------------------+       +-----------------------------------------+
|  useState('home' | 'form')         |       |  app/                                   |
|  - Повна втрата стану при unmount  |  vs   |  ├── (tabs)/index.tsx  (Маршрут "/")    |
|  - Відсутні нативні жести/свайпи   |       |  └── create-trip.tsx   (Маршрут "/...") |
|  - Неможливий Deep Linking         |       +-----------------------------------------+
|  - Складний спагеті-код переходів  |       | • Нативна оптимізація пам'яті (Screens) |
+------------------------------------+       | • Збереження стану та позиції скролу    |
                                             | • Нативна підтримка Deep Linking та URL |
                                             +-----------------------------------------+
Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

title "Архітектура мобільної навігації: Ручний стан vs Expo Router"

package "Антипатерн: Ручний useState" #fee2e2 {
  [App Component] as AC #fca5a5
  [HomeScreen] as HS1 #fecaca
  [FormScreen] as FS1 #fecaca
  
  AC -down-> HS1 : currentScreen === 'home'
  AC -down-> FS1 : currentScreen === 'form' (Unmounts HS1!)
  note bottom of HS1
    - Втрата стану скролу
    - Повторний запит до API
    - Відсутні нативні жести
    - Неможливий Deep Linking
  end note
}

package "Expo Router (File-Based Engine)" #dcfce7 {
  node "Файлова система (app/)" as FS #bbf7d0 {
    [app/(tabs)/index.tsx] as R1
    [app/create-trip.tsx] as R2
  }
  
  node "Expo Router Core" as ERC #86efac {
    [Route Resolver & Typed AST] as RR
    [React Navigation Container] as RNC
  }
  
  node "Нативний рівень (react-native-screens)" as NL #4ade80 {
    [UINavigationController (iOS)] as UI_NC
    [FragmentTransaction (Android)] as FR_NC
  }
  
  FS -down-> RR : компіляція маршрутів
  RR -down-> RNC : серіалізація стану графа
  RNC -down-> UI_NC : апаратні транзиції 60/120 FPS
  RNC -down-> FR_NC : апаратні транзиції 60/120 FPS
}
@enduml

Рівні абстракції: React Navigation та Expo Router

Для вирішення цих викликів у спільноті React Native було створено бібліотеку React Navigation — де-факто стандарт навігації, який забезпечує низькорівневі нативні примітиви (стековий навігатор на базі react-native-screens, нижні панелі вкладок, бокові панелі Drawer).

Expo Router побудований як надбудова поверх React Navigation. Він усуває необхідність громіздкої імперативної конфігурації екранів через велетенські JS-об'єкти чи вкладені JSX-структури (<NavigationContainer>, <Stack.Navigator>, <Stack.Screen>), замінюючи її декларативним зіставленням файлової системи. Фреймворк автоматично генерує граф переходів, серіалізує стан навігації в URL-подібну структуру та надає повну підтримку універсального рендерингу (iOS, Android, Web).

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


Концептуальний місток: Веб-орієнтований React проти Expo Router

Для спеціалістів, які мають досвід роботи з веб-версією React (зокрема з React Router DOM або Next.js), концепції Expo Router будуть інтуїтивно близькими. Проте через специфіку функціонування мобільних операційних систем існують важливі архітектурні відмінності:

Концепція у Web (Next.js / React Router)Відповідник у Expo RouterІнженерний коментар та семантика
Браузерний URL /aboutМаршрут /about (файл app/about.tsx)Шлях у файловій системі визначає точку входу в застосунок.
Каталог сторінок app/ у Next.jsКаталог app/ у корені проєктуКонцепція File-Based Routing: файли автоматично стають маршрутами.
Тег <a href="..."> або <Link> з Next.jsКомпонент <Link href="..."> з expo-routerДекларативний механізм навігації з підтримкою доступності (Accessibility).
Метод router.push('/path')Метод router.push('/path') з useRouter()Імперативний перехід, що виконується в обробниках подій та колбеках.
Файл макета layout.tsx (Next.js)Файл макета _layout.tsxКомпонентна оболонка сегмента; зберігає стан під час перемикання дітей.
Вкладені макети (Nested Layouts)Вкладені _layout.tsx у піддиректоріяхДозволяє комбінувати різні типи навігаторів (наприклад, Tabs усередині Stack).
Query/Path-параметри URLХуки useLocalSearchParams та useGlobalSearchParamsПередача динамічних аргументів між екранами.
Критичне застереження. Пакет react-router-dom, призначений для веб-браузерів, категорично не використовується в розробці під React Native. Веб-роутери маніпулюють DOM-деревом (window.history, document.location), які відсутні в нативному середовищі. Навігація в мобільних додатках здійснюється через пакет expo-router, який транслює дії в нативні команди мобільної ОС через бібліотеку react-native-screens.

Переваги підходу «Файл як маршрут» (File-as-a-Route)

Організація структури застосунку на основі файлового роутингу надає суттєві переваги для масштабування кодової бази та командної взаємодії:

  1. Самодокументована архітектура: Дерево каталогів усередині app/ є наочною ментальною картою застосунку. Будь-який розробник одразу розуміє, які екрани існують у продукті та як вони згруповані.
  2. Відсутність централізованого монолітного реєстру: Зникає потреба підтримувати велетенські файли конфігурації навігації, де вручну реєструються сотні екранів та зв'язків між ними (що в командах часто ставало джерелом конфліктів злиття у системі контролю версій Git).
  3. Природна уніфікація Deep Linking: Оскільки кожен екран автоматично прив'язаний до статичного або динамічного рядкового шляху, конфігурація вхідних посилань (наприклад, myapp://trip/123) починає працювати автоматично без ручного написання складних регулярних виразів для парсингу URL.

Водночас необхідно враховувати базові правила файлового роутера:

  • Не кожен файл у директорії app/ є кінцевим екраном: файли з префіксом підкреслення (зокрема _layout.tsx) виконують службову роль макетів-оболонок.
  • Назви директорій у круглих дужках (group) є логічними групами й не створюють сегментів у фінальному URL.
  • Розробник зобов'язаний явно вказувати тип навігаційного контейнера для кожного рівня (Stack чи Tabs) та конфігурувати заголовки й безпечні зони.


Фізична організація проєкту: Сегрегація директорій app/ та src/

У структурі архітектури нашого навчального курсу встановлено суворе розділення відповідальностей між двома ключовими каталогами:

                            АРХІТЕКТУРНА СЕГРЕГАЦІЯ
+---------------------------------------+   +---------------------------------------+
|                 app/                  |   |                 src/                  |
|        (Навігаційний рівень)          |   |       (Рівень бізнес-логіки)          |
+---------------------------------------+   +---------------------------------------+
| • Маршрутизація та точки входу        |   | • Доменні сутності (Features)         |
| • Конфігурація нативних макетів       |   | • UI-компоненти та дизайн-система     |
| • _layout.tsx (Stack, Tabs)           |   | • Стан, хуки, схеми валідації         |
| • «Тонкі контролери» (Thin Screens)   |   | • Чисті сервіси та утиліти            |
+---------------------------------------+   +---------------------------------------+
                   \                                     /
                    \--- Імпортує необхідні сутності ---/
  • Директорія app/ — шар навігації та макетів: Містить виключно файли маршрутів і файли макетів (_layout.tsx). Компоненти, розташовані тут, повинні залишатися «тонкими екранами» (Thin Screens). Їхнє єдине завдання — отримати навігаційні параметри (наприклад, через хук useLocalSearchParams), підключити відповідні доменні хуки або сервіси та змонтувати готові складені компоненти з шару src/.
  • Директорія src/ — шар бізнес-логіки, доменних фіч та дизайн-системи: Містить ізольовані модулі функціоналу (features/trips), спільні UI-компоненти (shared/ui), конфігурацію теми (shared/theme) та клієнти взаємодії з даними. Цей шар нічого не знає про файлову структуру app/, що забезпечує високий рівень повторного використання коду та спрощує модульне тестування.

Якщо розміщувати важку бізнес-логіку, стан форм чи складні алгоритми безпосередньо у файлах app/, екрани швидко перетворюються на важкі антипатерні модулі (God Objects), які неможливо повторно перевикористати або протестувати в ізоляції.

Структура каталогу проєкту Nomad після впровадження повноцінної навігації:

app/
  _layout.tsx           ← Кореневий макет: глобальні провайдери + головний Stack
  (tabs)/
    _layout.tsx         ← Навігатор нижніх вкладок (Tabs Layout)
    index.tsx           ← Маршрут "/" (Головна вкладка «Поїздки»)
    places.tsx          ← Маршрут "/places" (Вкладка «Місця»)
    about.tsx           ← Маршрут "/about" (Вкладка «Ще»)
  create-trip.tsx       ← Маршрут "/create-trip" (Екран у Stack поверх вкладок)
src/
  features/trips/       ← Доменна бізнес-логіка поїздок (моделі, форми, картки)
  shared/               ← Спільні компоненти UI, токени теми та утиліти
Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

title "Архітектурна сегрегація: app/ (Routing) vs src/ (Domain Core)"

package "app/ — Навігаційний рівень (Thin Controllers)" #e0f2fe {
  class "_layout.tsx (Root)" as RL #bae6fd {
    + ThemeProvider
    + TripsProvider
    + Stack Navigator
  }
  
  package "(tabs)/" as TABS_DIR #bae6fd {
    class "_layout.tsx (Tabs)" as TL #7dd3fc
    class "index.tsx (/) " as T_IDX #7dd3fc
    class "places.tsx (/places)" as T_PLC #7dd3fc
    class "about.tsx (/about)" as T_ABT #7dd3fc
  }
  
  class "create-trip.tsx (/create-trip)" as CRT #7dd3fc
}

package "src/ — Шар бізнес-логіки та дизайн-системи" #fef3c7 {
  package "features/trips" as FT #fde68a {
    class "TripsProvider & useTrips" as TP #fcd34d
    class "TripList, TripCard" as TC #fcd34d
    class "TripForm & Schema" as TF #fcd34d
  }
  
  package "shared/" as SH #fde68a {
    class "ThemeProvider & useTheme" as TH #fcd34d
    class "UI Kit (Button, Card, Input)" as UI #fcd34d
    class "Theme Tokens (colors, spacing)" as TT #fcd34d
  }
}

RL -down-> TH : монтує провайдер
RL -down-> TP : монтує провайдер
T_IDX -down-> TC : рендерить список поїздок
CRT -down-> TF : рендерить форму створення
T_PLC -down-> UI : використовує компоненти

note right of RL
  Правило чистої архітектури:
  app/ імпортує сутності з src/
  src/ НІКОЛИ не імпортує з app/
end note
@enduml

Семантичні одиниці: Маршрут (Route) та Екран (Screen)

Для побудови коректної ментальної моделі необхідно чітко розрізняти дві базові абстракції:

  1. Маршрут (Route): Це абстрактна логічна адреса (ідентифікатор) у навіційній системі застосунку, наприклад /, /places або /create-trip. Маршрут визначає, куди спрямовується навігаційний потік і як цей стан серіалізується для механізмів Deep Linking або історії переходів.
  2. Екран (Screen): Це конкретний візуальний React-компонент, який монтується та відмальовується у вікні перегляду, коли відповідний маршрут стає активним. У системі Expo Router екран завжди декларується як експорт за замовчуванням (export default function Screen()) із відповідного файлу в app/.

Хоча на мобільних платформах користувач не взаємодіє з текстовим рядком URL напряму (на відміну від веб-версії, де URL синхронізується з адресним рядком браузера), уся навігаційна підсистема функціонує за тими самими принципами просторової адресації.


_layout.tsx — Декларативна оболонка та межа сегмента

Файл з конвенційною назвою _layout.tsx є ключовим будівельним блоком Expo Router. Він розміщується в корені директорії app/ або в будь-якій її піддиректорії.

На відміну від звичайних файлів, _layout.tsx ніколи не стає самостійною кінцевою сторінкою в навігації. Це постійний компонент-обгортка (Layout Wrapper), який обрамляє всі маршрути поточної директорії та її підкаталогів.

                           АРХІТЕКТУРА МАКЕТІВ
+-------------------------------------------------------------------------+
| _layout.tsx (Кореневий макет: ThemeProvider + TripsProvider + Stack)    |
|                                                                         |
|  +-------------------------------------------------------------------+  |
|  | (tabs)/_layout.tsx (Вкладений макет: Tabs Navigator)              |  |
|  |                                                                   |  |
|  |  [index.tsx (Поїздки)]  [places.tsx (Місця)]  [about.tsx (Ще)]    |  |
|  +-------------------------------------------------------------------+  |
|                                                                         |
|  +-------------------------------------------------------------------+  |
|  | create-trip.tsx (Stack Screen поверх панелі вкладок)              |  |
|  +-------------------------------------------------------------------+  |
+-------------------------------------------------------------------------+

Основні інженерні обов'язки кореневого макета (app/_layout.tsx):

  1. Ініціалізація глобальних провайдерів стану: Підключення контекстів теми (ThemeProvider), глобальних доменних сховищ (TripsProvider), клієнтів кешування даних чи авторизації.
  2. Конфігурація кореневого навігаційного контейнера: Декларація типу головного навігатора застосунку (найчастіше це кореневий Stack, який керує переходами між основними розділами та модальними вікнами).
  3. Управління системними елементами ОС: Конфігурація нативного статус-бара (StatusBar), глобальних фонових кольорів вікна та обробка нативних екранів завантаження (Splash Screen).

Життєвий цикл макета та збереження стану

Найважливіша перевага _layout.tsx полягає в його персистентності. Коли користувач здійснює навігацію між дочірніми маршрутами (наприклад, переходить із головного екрана / на форму /create-trip), сам компонент _layout.tsx не розмонтовується.

Це гарантує, що:

  • Усі вкладені React-провайдери зберігають свій актуальний стан у пам'яті (тема оформлення, закешовані списки та дані авторизації не ініціалізуються повторно).
  • Не відбувається зайвих повторних рендерів глобальних підсистем застосунку.

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

title "Ієрархія макетів _layout.tsx та збереження стану контекстів"

rectangle "app/_layout.tsx (Root Layout — Завжди змонтований)" as ROOT #dbeafe {
  rectangle "ThemeProvider" as TP #bfdbfe {
    rectangle "TripsProvider" as TRP #93c5fd {
      rectangle "Root <Stack screenOptions={...}>" as RS #60a5fa {
        
        rectangle "app/(tabs)/_layout.tsx (Tabs Layout)" as TL #e9d5ff {
          rectangle "<Tabs screenOptions={...}>" as TABS_NAV #d8b4fe {
            rectangle "app/(tabs)/index.tsx\n[Екран Поїздки]" as S_TRIPS #c084fc
            rectangle "app/(tabs)/places.tsx\n[Екран Місця]" as S_PLACES #c084fc
            rectangle "app/(tabs)/about.tsx\n[Екран Ще]" as S_ABOUT #c084fc
          }
        }
        
        rectangle "app/create-trip.tsx\n[Екран форми створення]" as S_CREATE #fca5a5
      }
    }
  }
}

S_TRIPS .right.> S_CREATE : router.push('/create-trip')\n(Stack push)
S_CREATE .left.> S_TRIPS : router.back()\n(Stack pop)

note bottom of TL
  Tabs Layout зберігає локальний стан
  та позицію скролу між вкладками
end note

note bottom of ROOT
  Провайдери стану не розмонтовуються
  при переході між будь-якими екранами
end note
@enduml
_layout.tsx
спеціальний файл-макет
Декларативна оболонка сегмента файлової системи. Префікс нижнього підкреслення є конвенцією Expo Router, що виключає файл зі списку доступних публічних маршрутів. Тіло компонента зазвичай повертає навігатор (<Stack /> або <Tabs />), передаючи спільні опції всім дочірнім екранам.
Slot
компонент виведення дочірніх маршрутів
Базовий примітив Expo Router (import { Slot } from 'expo-router'), що слугує геометричним місцем рендерингу активного дочірнього маршруту сегмента, якщо розробник будує кастомний макет без використання вбудованих нативних контейнерів Stack або Tabs.
screenOptions
об’єкт або функція конфігурації
Універсальний словник параметрів за замовчуванням для всіх екранів даного навігатора. Дозволяє централізовано визначити видимість заголовків (headerShown), палітру кольорів навігаційної смуги, анімаційні переходи та нативні стилі тексту.

Приклад конфігурації кореневого макета

У наведеному лістингу продемонстровано архітектурний патерн правильного розташування глобальних провайдерів навколо стекового навігатора:

import { Stack } from 'expo-router';
import { StatusBar } from 'expo-status-bar';
import { TripsProvider } from '@/features/trips';
import { ThemeProvider, useTheme } from '@/shared/theme';

function RootNavigator() {
  const { colors, scheme } = useTheme();

  return (
    <>
      <StatusBar style={scheme === 'dark' ? 'light' : 'dark'} />
      <Stack
        screenOptions={{
          headerStyle: { backgroundColor: colors.background },
          headerTintColor: colors.primary,
          headerTitleStyle: { color: colors.text, fontWeight: '600' },
          contentStyle: { backgroundColor: colors.background },
        }}
      >
        {/* Група вкладок: заголовок кореневого стека приховується */}
        <Stack.Screen name="(tabs)" options={{ headerShown: false }} />
        {/* Окремий екран форми: заголовок стека активний з кнопкою повернення */}
        <Stack.Screen
          name="create-trip"
          options={{
            title: 'Нова поїздка',
            headerShown: true,
            presentation: 'card',
            headerBackTitle: 'Назад',
          }}
        />
      </Stack>
    </>
  );
}

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

Зверніть увагу: провайдери ThemeProvider та TripsProvider обгортають компонент RootNavigator. Завдяки цьому дочірні екрани обох гілок (як вкладки (tabs), так і окрема форма create-trip) мають однаковий доступ до контекстів через призначені хуки useTheme() та useTrips().


Конвенції файлової структури та маршрутизація index

Expo Router використовує строгі конвенції зіставлення шляхів файлів із маршрутами застосунку:

Шлях до файлу на дискуЗгенерований маршрут у системіІнженерне призначення
app/index.tsx/Головний стартовий екран кореневого рівня.
app/places.tsx/placesОкремий статичний екран першого рівня.
app/create-trip.tsx/create-tripОкремий екран створення поїздки.
app/(tabs)/index.tsx/Головний екран групи (tabs), що відповідає кореневому URL /.
app/(tabs)/places.tsx/placesЕкран списку місць усередині групи вкладок.
app/city/[id].tsx/city/:id (напр. /city/kyiv)Динамічний параметризований маршрут.

Семантична роль файлів index.tsx

Файл з назвою index.tsx завжди позначає кореневий (дефолтний) маршрут того каталогу, в якому він розташований. Якщо такий файл знаходиться безпосередньо в app/index.tsx або всередині групи app/(tabs)/index.tsx, він стає точкою входу для базового шляху /.

Рекомендації щодо найменування файлів:

  • Для назв файлів слід використовувати виключно стиль kebab-case (наприклад, create-trip.tsx, user-profile.tsx), оскільки імена файлів безпосередньо формують сегменти URL.
  • Категорично заборонено використовувати пробіли, службові символи та символи нелатинських абеток (кирилицю) в іменах файлів маршрутів.

Навігатор Stack: Модель стекової стопки екранів (LIFO)

Stack-навігатор (Navigation Stack) моделює поведінку просторової стопки карток за класичним принципом інформатики LIFO (Last In, First Out — останнім прийшов, першим пішов).

Коли користувач ініціює перехід до нового екрана, новий інтерфейсний шар «наштовхується» (push) на вершину стека, повністю або частково перекриваючи попередній. При поверненні назад верхній екран «знімається» зі стека (pop), відновлюючи видимість попереднього представлення разом із його незмінним станом пам'яті.

       ОПЕРАЦІЯ PUSH (router.push)                ОПЕРАЦІЯ POP (router.back)
+------------------------------------+       +------------------------------------+
|  [Вершина] create-trip.tsx         |  -->  |  [Знято зі стека і знищено]        |
+------------------------------------+       +------------------------------------+
|  [Основа]  (tabs) / index.tsx      |  <--  |  [Відновлено стан] (tabs)/index.tsx|
+------------------------------------+       +------------------------------------+
Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

title "Стековий навігатор (Stack): Життєвий цикл LIFO та переходи"

actor Користувач as User
participant "Екран: (tabs)/index" as ScreenA
participant "Expo Router Engine" as Router
participant "Native Stack Controller\n(UINavigationController / Fragment)" as NativeStack
participant "Екран: create-trip" as ScreenB

== Фаза 1: Перехід вперед (Push) ==
User -> ScreenA : Тап на кнопку "Нова поїздка"
ScreenA -> Router : router.push('/create-trip')
Router -> NativeStack : pushViewController(create-trip, animated: true)
NativeStack -> ScreenB : Монтування (Mount) & старт нативної анімації
note over ScreenA, ScreenB #e0f2fe
  Стек у пам'яті:
  1. [Вершина] create-trip.tsx (Активний, видимий)
  2. [Основа]  (tabs)/index.tsx (Прихований під стеком, стан збережено)
end note

== Фаза 2: Повернення назад (Pop) ==
User -> ScreenB : Збереження форми / жест "Назад"
ScreenB -> Router : router.back() (або Interactive Pop Gesture)
Router -> NativeStack : popViewController(animated: true)
NativeStack -> ScreenB : Розмонтування (Unmount) & очищення ресурсів
NativeStack -> ScreenA : Відновлення видимості (без повторного mount)
note over ScreenA #dcfce7
  Стек у пам'яті:
  1. [Вершина] (tabs)/index.tsx (Активний, скрол на місці)
end note
@enduml

Типові сценарії застосування Stack:

  • Ієрархічний перехід «Список сутностей → Детальна інформація про сутність».
  • Відкриття допоміжних форм введення даних або майстрів налаштування (Wizards).
  • Заглиблення в дерево налаштувань («Налаштування → Безпека → Зміна пароля»).

На рівні операційної системи Stack-навігатор Expo Router транслюється у високопродуктивні нативні контролери: UINavigationController в iOS та відповідні фрагментні транзакції FragmentTransaction в AndroidX. Це гарантує нативну швидкість анімацій зі швидкістю 60/120 кадрів на секунду та мінімальне навантаження на головний потік JavaScript.

Stack
компонент навігатора
Контейнер стекової навігації (import { Stack } from 'expo-router'). Дочірні файли поточної директорії автоматично реєструються як доступні екрани цього стека.
Stack.Screen
компонент конфігурації екрана
Декларативний елемент налаштування конкретного маршруту в стеку. Атрибут name повинен точно відповідати імені файлу маршруту (без розширення .tsx). Властивість options дозволяє задати параметри відображення заголовка, анімацій та кнопок.
options.presentation
'card' | 'modal' | 'transparentModal'
Визначає візуальну модальність появи екрана. Значення 'card' (стандартне) виконує класичний горизонтальний зсув картки праворуч. Значення 'modal' транслює екран у модальне вікно, що виїжджає знизу екрана (глибше розглядається в наступних розділах).
options.headerShown
boolean
Визначає видимість нативної смуги заголовка (UINavigationBar / Toolbar). Якщо екран використовує кастомний заголовок, зверстаний засобами React Native, цей параметр встановлюють у false.
options.headerBackTitle
string (iOS)
Конфігурує текстовий підпис біля стрілки повернення назад на платформі iOS (наприклад, «Назад» або назву попереднього екрана).

Механіка обробки системного повернення «Назад»

Нативна інтеграція Stack-навігатора забезпечує узгоджену поведінку з платформовими гайдлайнами:

  • iOS: Підтримується плавний інерційний жест повернення змахуванням від лівого краю екрана (Interactive Swipe-to-Back) та нативна кнопка повернення у смузі заголовка.
  • Android: Навігатор автоматично перехоплює натискання системної апаратної/програмної кнопки «Назад» або сучасний жест від краю екрана (Back Gesture), коректно викликаючи операцію pop для активного стека.

Програмний виклик методу router.back() ініціює аналогічну операцію: перевіряє історію переходів і повертає користувача на один рівень назад.


Навігатор Tabs: Модель паралельних розділів інтерфейсу

Навігатор вкладок (Bottom Tabs Navigator) організовує кілька рівноправних функціональних доменів застосунку, перемикання між якими здійснюється через фіксовану панель у нижній частині дисплея.

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

                     НАВІГАЦІЙНЕ ДЕРЕВО ВКЛАДОК
               +------------------------------------+
               |           Tabs Navigator           |
               +------------------------------------+
                 /                |               \
                /                 |                \
    +-----------------+  +-----------------+  +-----------------+
    | Вкладка:        |  | Вкладка:        |  | Вкладка:        |
    | «Поїздки» (/)   |  | «Місця» (/places)| | «Ще» (/about)   |
    +-----------------+  +-----------------+  +-----------------+
Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

title "Навігатор Tabs: Модель паралельних гілок та ізоляція стану"

rectangle "app/(tabs)/_layout.tsx <Tabs>" as TABS_CONTAINER #f3e8ff {
  
  rectangle "Вкладка 1: Trips (index.tsx)" as TAB1 #e9d5ff {
    rectangle "Скрол: Y = 450px" as S1 #d8b4fe
    rectangle "Вибраний фільтр: 'Активні'" as F1 #d8b4fe
  }
  
  rectangle "Вкладка 2: Places (places.tsx)" as TAB2 #e0e7ff {
    rectangle "Скрол: Y = 0px" as S2 #c7d2fe
    rectangle "Пошуковий запит: 'Карпати'" as Q2 #c7d2fe
  }
  
  rectangle "Вкладка 3: About (about.tsx)" as TAB3 #fae8ff {
    rectangle "Статичний контент" as S3 #f5d0fe
  }
}

actor Користувач as U
U -down-> TAB1 : 1. Переглядає список
U -down-> TAB2 : 2. Тап на вкладку "Місця" (Стан TAB1 НЕ скидається)
U -down-> TAB1 : 3. Повернення на "Поїздки" (Скрол 450px та фільтр збережено)

@enduml

Ергономічні вимоги до панелі вкладок:

  • Кожна вкладка повинна містити чітку семантичну векторну іконку та лаконічний підпис (до 10–12 символів).
  • Активний стан вкладки зобов'язаний візуально виділятися акцентним кольором теми бренду (tabBarActiveTintColor).
  • Перемикання вкладок повинно зберігати внутрішній стан кожної з них (наприклад, поточну позицію скролу списку поїздок при переході на вкладку місць і назад).
Tabs
компонент навігатора
Контейнер нижніх вкладок (import { Tabs } from 'expo-router'). Файл _layout.tsx усередині директорії вкладок експортує компонент, що повертає розмітку <Tabs>…</Tabs>.
Tabs.Screen
компонент конфігурації вкладки
Реєструє окрему вкладку в навігаційній панелі. Параметр name задає ім'я цільового файлу маршруту (index, places, about).
options.tabBarIcon
({ color, size, focused }) => ReactNode
Функція зворотного виклику, що повертає компонент векторної іконки. Expo Router автоматично передає в цю функцію обчислений колір (color) та рекомендований геометричний розмір (size) залежно від активності вкладки.
options.tabBarActiveTintColor / tabBarInactiveTintColor
ColorValue
Кольори для активного та неактивного станів іконок і підписів вкладки. Повинні синхронізуватися з поточною темою оформлення (colors.primary та colors.textSecondary).
options.tabBarStyle
ViewStyle
Об'єкт стилізації контейнера панелі вкладок (висота, колір фону, колір верхньої розділової межі). У темній темі вимагає обов'язкового налаштування для запобігання появі білої смуги на темному тлі контенту.

Інтеграція векторних іконок (@expo/vector-icons)

Для оформлення таб-бару використовується бібліотека @expo/vector-icons, яка постачається з популярними наборами гліфів (Ionicons, MaterialIcons, Feather, FontAwesome):

npx expo install @expo/vector-icons

Приклад конфігурації іконки у вкладці:

<Tabs.Screen
  name="places"
  options={{
    title: 'Місця',
    tabBarIcon: ({ color, size }) => (
      <Ionicons name="location-outline" size={size} color={color} />
    ),
  }}
/>

Довідник screenOptions: Конфігурація заголовків, стека та вкладок

У проєктах на базі Expo Router конфігурація візуального відображення та поведінки екранів здійснюється за допомогою універсального словника параметрів screenOptions (на рівні макета-навігатора) або властивості options (на рівні конкретного компонента <Stack.Screen /> чи <Tabs.Screen />).

Розуміння повної карти полів screenOptions критично важливе для побудови професійних інтерфейсів: від нативних розмитих шапок у стилі iOS до адаптивних таб-барів, системних статус-барів і модальних переходів.

Ієрархія та каскад спадкування конфігурацій

Налаштування екранів застосовуються за трирівневою каскадною моделлю з чітким пріоритетом:

[Рівень 1: Глобальні screenOptions] ──▶ [Рівень 2: Декларативні options екрана] ──▶ [Рівень 3: Рантайм setOptions()]
   (Базові стилі для всього стека)           (Перевизначення для конкретного маршруту)         (Динамічні зміни під час роботи)
Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

title "Анатомія screenOptions: Каскад спадкування та пріоритет перевизначення"

rectangle "Рівень 1: Глобальні налаштування навігатора\n<Stack screenOptions={{ ... }}> або <Tabs screenOptions={{ ... }}>" as L1 #dbeafe {
  note right
    - headerShown: true
    - headerStyle: { backgroundColor: colors.background }
    - headerTintColor: colors.primary
    - headerTitleStyle: { fontWeight: '600' }
    - contentStyle: { backgroundColor: colors.background }
  end note
}

rectangle "Рівень 2: Декларативне перевизначення для конкретного маршруту\n<Stack.Screen name='create-trip' options={{ ... }}>" as L2 #fef3c7 {
  note right
    - title: 'Нова поїздка'
    - presentation: 'modal'
    - headerBackTitle: 'Назад'
    - headerRight: () => <SaveButton />
  end note
}

rectangle "Рівень 3: Динамічне виконання під час рендеру\nnavigation.setOptions({ title: item.title, headerRight: ... })" as L3 #dcfce7 {
  note right
    - Динамічний заголовок за даними з API
    - Активність кнопки збереження (disabled / enabled)
    - Найвищий пріоритет у рантаймі!
  end note
}

L1 -down-> L2 : Спадкується за замовчуванням
L2 -down-> L3 : Перевизначається динамічно
@enduml

1. Загальні параметри нативного заголовка (Header Screen Options)

Ці параметри доступні як у Stack, так і в Tabs та відповідають за рендеринг верхньої навігаційної смуги (UINavigationBar в iOS / Toolbar в Android):

headerShown
boolean
Визначає видимість нативної смуги заголовка. Встановлення значення false повністю приховує заголовок операційної системи, передаючи керування верхньою частиною екрана верстці React Native або компоненту SafeAreaView.
headerTitle
string | ((props: HeaderTitleProps) => ReactNode)
Текстовий заголовок екрана або функціональний React-компонент для рендерингу складного вмісту по центру шапки (наприклад, логотипу, перемикача сегментів чи аватара з підписом).
headerTitleAlign
'left' | 'center'
Горизонтальне вирівнювання заголовка. За замовчуванням на платформі iOS заголовок центрується, тоді як на Android притискається до лівого краю.
headerTitleStyle
TextStyle
Об'єкт стилізації тексту заголовка: fontSize, fontWeight, fontFamily, color, letterSpacing.
headerTintColor
ColorValue
Колір відтінку інтерактивних елементів заголовка: іконки та тексту кнопки «Назад», іконок дій у headerLeft/headerRight та стандартного заголовка.
headerStyle
ViewStyle
Стиль контейнера панелі заголовка: backgroundColor, elevation (Android), shadowOpacity (iOS), borderBottomWidth, borderBottomColor.
headerTransparent
boolean
Вмикає абсолютне позиціонування заголовка поверх контенту екрана. При значенні true контент починається з верхнього краю дисплея y = 0 (ідеально для екранів з фотообкладинками та мапами).
headerBlurEffect
'regular' | 'prominent' | 'extraLight' | 'light' | 'dark' (iOS)
Вмикає нативне розмиття фону заголовка в стилі iOS (застосовується разом із напівпрозорим фоном або headerTransparent: true).
headerBackground
() => ReactNode
Функція повернення кастомного фонового компонента для хедера (наприклад, BlurView з expo-blur або градієнта LinearGradient).
headerLeft
((props: { tintColor?: string, canGoBack?: boolean, label?: string }) => ReactNode)
Кастомний React-компонент для лівої частини заголовка (замінює або доповнює стандартну кнопку повернення «Назад»).
headerRight
((props: { tintColor?: string }) => ReactNode)
Кастомний React-компонент для правої частини заголовка (кнопки збереження, іконки фільтрів, шестерня налаштувань, аватар профілю).
headerBackVisible
boolean
Чи показувати стандартну стрілку/кнопку повернення «Назад». Якщо встановити в false, кнопка приховується навіть за наявності попередніх екранів у стеку.
headerBackTitle
string (iOS)
Текстовий підпис кнопки повернення на платформі iOS (наприклад, «Назад» або «Скасувати»).
headerBackTitleVisible
boolean (iOS)
Чи відображати текстовий підпис біля стрілки «Назад» на iOS (якщо false, відображається лише шеврон без тексту).
headerBackTitleStyle
TextStyle (iOS)
Стиль шрифту текстового підпису кнопки «Назад».
headerShadowVisible
boolean
Чи відображати нативну тінь (iOS) або смугу розділення/elevation (Android) під заголовком. Значення false робить перехід між заголовком і тілом екрана безшовним.
headerLargeTitle
boolean (iOS)
Вмикає підтримку великого заголовка в стилі iOS (Large Title), який плавно згортається у звичайний заголовок під час скролу списку.
headerLargeTitleStyle
TextStyle (iOS)
Стиль тексту великого заголовка (наприклад, fontSize: 34, fontWeight: '700').
headerLargeTitleShadowVisible
boolean (iOS)
Видимість розділової тіні під великим заголовком.
headerSearchBarOptions
HeaderSearchBarOptions (iOS/Android)
Інтегрує нативний рядок пошуку безпосередньо в заголовок (UISearchController на iOS / SearchView на Android). Приймає об'єкт з полями placeholder, onChangeText, onSearchButtonPress, hideWhenScrolling, tintColor.
headerPressColorAndroid
string (Android)
Колір хвильового ефекту торкання (Ripple effect) для кнопок у заголовку на платформі Android.

2. Специфічні параметри стекового навігатора (Stack Screen Options)

Ці параметри конфігурують поведінку LIFO-стека, модальність вікон, системні статус-бари та анімації переходу нативного стека (@react-navigation/native-stack):

presentation
'card' | 'modal' | 'transparentModal' | 'containedModal' | 'fullScreenModal' | 'formSheet'
Тип візуального представлення та модальності екрана:
  • 'card' — стандартний екран стека з горизонтальним зсувом;
  • 'modal' — нативне модальне вікно (на iOS з'являється знизу з ефектом масштабування батьківського екрана);
  • 'transparentModal' — модальне вікно з прозорим фоном, крізь який видно попередній екран;
  • 'fullScreenModal' — модальне вікно, що перекриває весь дисплей включно зі статус-баром;
  • 'formSheet' — модальний лист у стилі iPad/планшетів.
animation
'default' | 'fade' | 'fade_from_bottom' | 'flip' | 'simple_push' | 'slide_from_bottom' | 'slide_from_right' | 'slide_from_left' | 'none'
Тип нативної анімації переходу між екранами:
  • 'slide_from_right' — класичний перехід Android/iOS;
  • 'slide_from_bottom' — виїзд знизу;
  • 'fade' — плавне згасання/поява;
  • 'none' — миттєве перемикання без анімації.
animationDuration
number (мс)
Тривалість нативної анімації переходу в мілісекундах.
gestureEnabled
boolean
Чи увімкнений нативний жест повернення змахуванням (Interactive Pop Gesture на iOS або свайп назад).
gestureDirection
'horizontal' | 'vertical' | 'horizontal-inverted' | 'vertical-inverted'
Напрямок жесту пальця для повернення на попередній екран.
fullScreenGestureEnabled
boolean (iOS)
Дозволяє здійснювати жест повернення назад свайпом з будь-якої точки дисплея, а не лише від самого лівого краю.
contentStyle
ViewStyle
Стиль кореневого нативного контейнера екрана (backgroundColor: colors.background). Запобігає білим спалахам під час анімацій у темній темі.
autoHideHomeIndicator
boolean (iOS)
Автоматичне приховування смуги домашнього індикатора жестів на iOS без рамки.
orientation
'default' | 'all' | 'portrait' | 'portrait_up' | 'portrait_down' | 'landscape' | 'landscape_left' | 'landscape_right'
Примусова орієнтація екрана (наприклад, блокування в портретному режимі для форми або альбомному для перегляду медіа).
statusBarColor
string (Android)
Колір фону системного статус-бара на платформі Android.
statusBarStyle
'auto' | 'inverted' | 'light' | 'dark'
Стиль іконок і тексту системного статус-бара ('light' — білий текст для темного фону, 'dark' — темний текст для світлого).
statusBarHidden
boolean
Чи приховувати системний статус-бар при переході на даний екран.
statusBarTranslucent
boolean (Android)
Чи робити статус-бар напівпрозорим на Android, дозволяючи контенту рендеритися під ним.
navigationBarColor
string (Android)
Колір фону нижньої системної панелі керування операційної системи Android.
navigationBarHidden
boolean (Android)
Чи приховувати нижню системну навігаційну смугу на Android.
freezeOnBlur
boolean
Оптимізація пам'яті через бібліотеку react-freeze: заморожує повторний рендеринг компонентів екрана, коли він перекритий іншим екраном стека.

3. Специфічні параметри панелі вкладок (Bottom Tabs Options)

Ці параметри керують конфігурацією нижньої панелі вкладок <Tabs /> (@react-navigation/bottom-tabs):

tabBarShowLabel
boolean
Чи показувати текстові підписи під іконками вкладок. Якщо встановити у false, таб-бар відображатиме виключно іконки.
tabBarLabel
string | ((props: { focused: boolean, color: string, position: LabelPosition, children: string }) => ReactNode)
Текстовий підпис вкладки або кастомна функція його рендерингу (перевизначає параметр title спеціально для нижньої панелі).
tabBarLabelPosition
'below-icon' | 'beside-icon'
Позиція текстового підпису: під іконкою чи праворуч від неї (актуально для планшетів та ландшафтного режиму).
tabBarLabelStyle
TextStyle
Об'єкт стилізації тексту вкладки (fontSize, fontWeight, fontFamily).
tabBarIcon
((props: { focused: boolean, color: string, size: number }) => ReactNode)
Функція зворотного виклику для рендерингу векторної іконки вкладки. Отримує обчислений стан фокусу focused, розмір size (зазвичай 24–28) та колір color.
tabBarBadge
string | number
Бейдж над іконкою вкладки (наприклад, лічильник непрочитаних сповіщень 3 або точка '!').
tabBarBadgeStyle
TextStyle
Стиль бейджа сповіщень: backgroundColor, color, fontSize.
tabBarActiveTintColor / tabBarInactiveTintColor
ColorValue
Колір іконки та тексту для активної (вибраної) та неактивної вкладки відповідно.
tabBarActiveBackgroundColor / tabBarInactiveBackgroundColor
ColorValue
Колір фону окремого елемента вкладки в активному та неактивному станах.
tabBarStyle
ViewStyle
Стиль контейнера нижньої панелі вкладок: backgroundColor, height, borderTopColor, position: 'absolute', bottom, borderRadius (для створення плаваючого таб-бару з тінню).
tabBarBackground
() => ReactNode
Кастомний фоновий компонент для панелі вкладок (наприклад, BlurView для напівпрозорого розмиття контенту під панеллю).
tabBarButton
((props: BottomTabBarButtonProps) => ReactNode)
Кастомний React-компонент кнопки вкладки (використовується для створення опуклих центральних кнопок «+» або кастомних анімацій при тапі).
tabBarItemStyle
ViewStyle
Стилізація контейнера кожного окремого елемента вкладки.
tabBarHideOnKeyboard
boolean (Android)
Чи приховувати панель вкладок при відкритті екранної клавіатури, запобігаючи її накладанню на поля вводу.
tabBarAccessibilityLabel
string
Текстовий підпис доступності для екранних дикторів VoiceOver / TalkBack.
lazy
boolean
Ліниве монтування екранів вкладок: екран рендериться лише тоді, коли користувач вперше переходить на відповідну вкладку.
unmountOnBlur
boolean
Чи повністю розмонтовувати екран при переході на іншу вкладку. За замовчуванням false (стан та скрол зберігаються); встановлення true очищує пам'ять та скидає стан екрана.

4. Динамічна функціональна форма screenOptions та хук useNavigation

Параметр screenOptions може приймати не лише статичний об'єкт, а й функцію зворотного виклику, яка отримує поточний маршрут (route) та об'єкт навігації (navigation):

<Stack
  screenOptions={({ route }) => ({
    headerTitle: route.name === 'create-trip' ? 'Створення' : 'Nomad',
    headerShown: !route.name.startsWith('('),
  })}
/>

Крім того, будь-який компонент екрана всередині app/ може динамічно оновлювати власні options під час роботи за допомогою хука useNavigation:

import { useLayoutEffect } from 'react';
import { useNavigation } from 'expo-router';
import { Pressable, Text } from 'react-native';

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

  useLayoutEffect(() => {
    navigation.setOptions({
      title: 'Редагування поїздки',
      headerRight: () => (
        <Pressable onPress={() => alert('Збережено!')}>
          <Text style={{ color: '#2563eb', fontWeight: '600' }}>Готово</Text>
        </Pressable>
      ),
    });
  }, [navigation]);

  return null;
}

Логічне групування маршрутів: Семантика директорій (group)

У структурі файлового роутера назви директорій, взяті в круглі дужки, наприклад app/(tabs)/ або app/(auth)/, називаються групами маршрутів (Route Groups).

Круглі дужки є спеціальним синтаксичним маркером, який повідомляє компілятору Expo Router: ця директорія слугує виключно для логічної організації файлів та ізоляції макетів, але не додає власний сегмент до фінального рядка URL-маршруту.

  ФАЙЛОВА СТРУКТУРА НА ДИСКУ                     РЕЗУЛЬТУЮЧИЙ МАРШРУТ (URL)
+-----------------------------------+       +-----------------------------------+
| app/                              |       |                                   |
| └── (tabs)/                       |  -->  | (Сегмент "(tabs)" ігнорується)   |
|     ├── index.tsx                 |  -->  | "/"                               |
|     ├── places.tsx                |  -->  | "/places"                         |
|     └── about.tsx                 |  -->  | "/about"                          |
+-----------------------------------+       +-----------------------------------+
Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

title "Семантика груп маршрутів (group): Ізоляція макетів без сегмента URL"

node "Файлова система на диску" as FS #f8fafc {
  folder "app/" as APP #e2e8f0 {
    folder "(tabs)/ [Група вкладок]" as G_TABS #dbeafe {
      file "_layout.tsx" as TL_F #bfdbfe
      file "index.tsx" as T1_F #bfdbfe
      file "places.tsx" as T2_F #bfdbfe
      file "about.tsx" as T3_F #bfdbfe
    }
    folder "(auth)/ [Група авторизації]" as G_AUTH #fef3c7 {
      file "_layout.tsx" as AL_F #fde68a
      file "login.tsx" as A1_F #fde68a
      file "register.tsx" as A2_F #fde68a
    }
    file "create-trip.tsx" as CR_F #fee2e2
  }
}

node "URL-простір маршрутів (Public Routes)" as URLS #f0fdf4 {
  [Маршрут: "/"] as U_ROOT #bbf7d0
  [Маршрут: "/places"] as U_PLC #bbf7d0
  [Маршрут: "/about"] as U_ABT #bbf7d0
  [Маршрут: "/login"] as U_LOG #bbf7d0
  [Маршрут: "/register"] as U_REG #bbf7d0
  [Маршрут: "/create-trip"] as U_CRT #bbf7d0
}

T1_F --> U_ROOT : дужки (tabs) опускаються
T2_F --> U_PLC : дужки (tabs) опускаються
T3_F --> U_ABT : дужки (tabs) опускаються
A1_F --> U_LOG : дужки (auth) опускаються
A2_F --> U_REG : дужки (auth) опускаються
CR_F --> U_CRT : пряме відображення
@enduml

Якби директорія називалася без дужок app/tabs/places.tsx, фінальний маршрут набув би вигляду /tabs/places, що створювало б зайву вкладеність в адресації.

Архітектурні сценарії використання груп:

  1. Ізоляція макетів (Layout Scoping): Об'єднання екранів, які повинні мати спільну панель вкладок Tabs, окремо від екранів, які повинні відображатися на весь дисплей у Stack.
  2. Сегрегація авторизаційних потоків: Створення груп (auth) (для екранів входу та реєстрації зі своїм макетом) та (app) (для основної робочої зони авторизованого користувача).
  3. Підтримка чистоти кореневого простору імен: Дозволяє утримувати лаконічні та семантично чисті URL першого рівня (/, /places, /profile), одночасно структурувавши десятки файлів проєкту за окремими підкаталогами.

Expo Router надає два взаємодоповнюючі механізми для реалізації переходів між маршрутами: декларативний (компонент Link) та імперативний (хук useRouter).

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

title "Декларативний Link vs Імперативний useRouter: Цикл обробки переходу"

actor Користувач as U
participant "UI View (<Link> / <Pressable>)" as UI
participant "useRouter / Link Dispatcher" as Disp
participant "Expo Router State Engine" as Engine
participant "React Navigation Controller" as RNC
participant "Цільовий екран (Target Screen)" as Target

alt Декларативний перехід: <Link href="/create-trip" asChild>
  U -> UI : Тап на елемент посилання
  UI -> Disp : onPress обробник від Link
else Імперативний перехід: router.push('/create-trip')
  U -> UI : Тап на кнопку форми
  UI -> UI : Валідація схеми форми
  UI -> Disp : router.push('/create-trip')
end

Disp -> Engine : dispatch({ type: 'NAVIGATE', payload: { name: 'create-trip' } })
Engine -> Engine : Перевірка Typed Routes & пошук маршруту в AST
Engine -> RNC : Виклик нативної стекової операції Push
RNC -> Target : Монтування компонента Target + запуск транзиції
Target --> U : Відображення нового екрана (60/120 FPS)
@enduml

Компонент Link призначений для розміщення навігаційних посилань безпосередньо в JSX-дереві. Цей підхід подібний до веб-елемента гіперпосилання, забезпечуючи високу декларативність та повну підтримку систем доступності для екранних дикторів (Screen Readers).

Базовий синтаксис:

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

<Link href="/create-trip">
  <Text>Створити нову поїздку</Text>
</Link>

Властивість asChild та інтеграція з кастомними кнопками

За замовчуванням компонент Link огортає переданий дочірній текст власним текстовим представленням. Якщо розробнику необхідно надати посиланню вигляд складної кнопки (Pressable), картки або інтерактивного блоку зі стилями, застосовується властивість asChild.

Властивість asChild повідомляє Link, що він не повинен створювати додатковий DOM/Native-вузол у дереві, а натомість зобов'язаний передати всі необхідні навігаційні обробники подій дотику (onPress) та атрибути доступності безпосередньо своєму першому дочірньому елементу:

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

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

2. Імперативний підхід: хук useRouter

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

  • Після успішної валідації та відправки форми на сервер.
  • У відповідь на асинхронні події (завершення таймера, отримання push-повідомлення).
  • За необхідності попередньої перевірки умов авторизації перед відкриттям екрана.
router.push(href)
метод: (href: Href) => void
Додає новий маршрут на вершину поточного навігаційного стека (Stack.push). Попередній екран залишається в історії, а на новому екрані автоматично стає доступною кнопка та жест повернення «Назад».
router.replace(href)
метод: (href: Href) => void
Замінює поточний запис у навігаційній історії новим маршрутом. Використовується після успішної автентифікації або скидання пароля, щоб користувач не міг повернутися на екран вводу облікових даних через кнопку «Назад».
router.back()
метод: () => void
Знімає верхній екран зі стека, здійснюючи повернення до попереднього стану навігаційної історії. Якщо форма створення зберегла поїздку, виклик router.back() закриває форму і показує оновлений список на вкладках.
router.canGoBack()
метод: () => boolean
Повертає логічне значення true, якщо в стеку навігації є хоча б один попередній екран, до якого можна повернутися. Запобігає виклику back() в умовах порожнього стека (наприклад, при прямому холодному старті за Deep Link).
router.dismiss(count?)
метод: (count?: number) => void
Закриває активне модальне вікно або знімає вказану кількість екранів із вершини стека.

Приклад застосування useRouter в обробнику подій:

import { useRouter } from 'expo-router';

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

  const handleFormComplete = async (tripData: unknown) => {
    // 1. Асинхронне збереження даних
    await saveTrip(tripData);
    
    // 2. Програмне повернення до попереднього списку
    if (router.canGoBack()) {
      router.back();
    } else {
      router.replace('/');
    }
  };

  return <TripForm onSubmit={handleFormComplete} />;
}

Матриця вибору навігаційного інструменту

Сценарій використанняРекомендований інструментОбґрунтування вибору
Елемент меню, посилання в тексті, статична картка<Link href="...">Чітка декларативність у розмірці, підтримка Screen Reader.
Кнопка зі складним кастомним оформленням<Link href="..." asChild><Pressable>…</Link>Повне збереження кастомних стилів кнопки без спагеті-коду в onPress.
Колбек завершення відправки формиrouter.back()Перехід ініціюється тільки після успішного виконання асинхронного коду.
Завершення процедури авторизації / виходуrouter.replace('/...')Очищення навігаційного стека від екранів авторизації.

Структура посилань href та статичні шляхи

Параметр href приймає рядок або об'єкт, який точно вказує цільову адресу маршруту в навігаційному дереві застосунку.

На етапі вивчення базової маршрутизації використовуються статичні шляхи:

  • "/" — кореневий маршрут (головний екран списку поїздок у групі (tabs));
  • "/places" — екран каталогу локацій;
  • "/about" — інформаційний екран «Про додаток»;
  • "/create-trip" — стековий екран створення поїздки.

Динамічні параметризовані сегменти (наприклад, app/city/[id].tsx, де :id виступає змінною частиною URL) детально досліджуються у наступних розділах курсу.

Вимога існування маршруту. Цільовий рядок href повинен гарантовано вказувати на існуючий фізичний файл у каталозі app/. Будь-яка механічна помилка в назві (наприклад, випадкова одруківка href="/crate-trip") призведе до помилки часу виконання «Unmatched Route» (маршрут не знайдено) та відображення білого екрана або системного екрана 404.

Статична типізація навігаційних шляхів (Typed Routes)

Для запобігання помилкам, пов'язаним з ручним введенням рядків href, Expo Router пропонує інструмент компіляційного контролю — Typed Routes (Типізовані маршрути).

При активації цієї функції компілятор Expo Router автоматично сканує вміст директорії app/ і на льоту генерує TypeScript-декларації глобального об'єкта шляхів, типізуючи пропси компонента Link та параметри методів router.push/router.replace.

Активація типізації у файлі app.json:

{
  "expo": {
    "name": "Nomad",
    "slug": "nomad",
    "experiments": {
      "typedRoutes": true
    }
  }
}

Після ввімкнення експериментального прапорця та перезапуску сервера розробки Metro (npx expo start), будь-яка спроба передати неіснуючий рядок шляху викличе помилку компілятора TypeScript ще до запуску застосунку на пристрої:

// TypeScript видасть помилку: Argument of type '"/unknown-screen"' is not assignable to parameter of type 'Href'
router.push('/unknown-screen'); 

// Успішна валідація з підтримкою автодоповнення в IDE:
router.push('/create-trip');

Координація нативних шарів: Header, SafeAreaView та розрахунок відступів

Під час конструювання мобільних інтерфейсів розробники регулярно стикаються з проблемою розрахунку верхніх і нижніх відступів під системні елементи пристрою: вирізи камер (Notch, Dynamic Island), рядок стану (Status Bar) та домашній індикатор жестів (Home Indicator).

Для ізоляції контенту від накладання на системні елементи використовується компонент SafeAreaView з бібліотеки react-native-safe-area-context. Проте при роботі з нативними навігаторами виникає специфічний конфлікт геометрії:

          КОНФЛІКТ ПОДВІЙНОГО ВІДСТУПУ (DOUBLE INSET BUG)
+-------------------------------------------------------------------------+
| [Status Bar / Notch / Dynamic Island]                                   |
| +---------------------------------------------------------------------+ |
| | Native Header (Вже містить власний безпечний відступ зверху)         | |
| +---------------------------------------------------------------------+ |
| | [ЗАЙВИЙ ВІДСТУП!] SafeAreaView edges={['top']} додає ще 44–59 pt     | |
| | +-----------------------------------------------------------------+ | |
| | | Контент форми (Невиправдано зміщений далеко вниз)               | | |
| | +-----------------------------------------------------------------+ | |
+-------------------------------------------------------------------------+
Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

title "Координація нативних шарів: Safe Area, Header та Viewport"

rectangle "Мобільний пристрій (Screen Frame)" as DEVICE #f1f5f9 {
  
  rectangle "Status Bar / Notch / Dynamic Island (44–59 pt)" as SB #cbd5e1
  
  rectangle "Native Header (<Stack.Screen options={{ headerShown: true }}>)" as NH #93c5fd {
    rectangle "Left: Back Button" as HL #60a5fa
    rectangle "Center: Title" as HT #60a5fa
    rectangle "Right: Action Button" as HR #60a5fa
  }
  
  rectangle "Основна робоча область (Viewport Content Area)" as CONTENT #f8fafc {
    rectangle "SafeAreaView edges={['bottom', 'left', 'right']}\n(Без повторного 'top' відступу!)" as SAV #bbf7d0 {
      rectangle "Контент форми / Списку" as DATA #86efac
    }
  }
  
  rectangle "Native Bottom Tab Bar (<Tabs.Screen />)" as TABS #d8b4fe {
    rectangle "Tab 1: Поїздки" as TB1 #c084fc
    rectangle "Tab 2: Місця" as TB2 #c084fc
    rectangle "Tab 3: Ще" as TB3 #c084fc
  }
  
  rectangle "Home Indicator / Bottom Inset (34 pt)" as HI #cbd5e1
}
@enduml

Правила координації геометрії:

  1. Екрани з активним нативним заголовком (headerShown: true): Нативний компонент заголовка Stack.Screen автоматично резервує необхідний простір під системний статус-бар. Якщо контент такого екрана додатково обгорнути в SafeAreaView з верхнім відступом, виникає дефект подвійного порожнього зазору (Double Padding Bug). Тому на таких екранах верхній край SafeAreaView повинен бути вимкнений (edges={['bottom', 'left', 'right']}).
  2. Екрани з прихованим заголовком (headerShown: false): На екранах вкладок (tabs), де системний заголовок вимкнено на користь власної верстки назви розділу, верхній відступ повинен повністю компенсуватися компонентом Screen або SafeAreaView (edges={['top']}).

Архітектурні антипатерни та типові помилки проєктування навігації

1. Емуляція навігації через локальний стан useState('screen')

  • Проблема: Спроба будувати навігацію великого додатку через умовний рендеринг за станом створює спагеті-код, руйнує кешування екранів, блокує апаратні жести повернення та унеможливлює Deep Linking.
  • Рішення: Використовувати виключно декларативні можливості Expo Router (app/, Stack, Tabs).

2. Концентрація складної бізнес-логіки у файлах маршрутів app/

  • Проблема: Перетворення файлів у app/ на монолітні компоненти обсягом понад 500 рядків унеможливлює повторне використання UI-блоків та ізольоване тестування.
  • Рішення: Зберігати маршрути в app/ максимально тонкими, делегуючи верстку та бізнес-логіку модулям із src/features/ та src/shared/.

3. Плутанина між логічними групами (group) та фізичними сегментами URL

  • Проблема: Очікування, що файл app/(tabs)/places.tsx буде доступний за маршрутом /tabs/places, і спроби звертатися до нього через цей некоректний шлях.
  • Рішення: Пам'ятати, що круглі дужки в іменах директорій виключаються з фінального рядка URL. Правильний шлях — /places.

4. Виклик push('/') замість back() після успішного збереження форми

  • Проблема: Якщо після збереження сутності у формі викликати router.push('/'), навігатор не повернеться на існуючий екран, а створить і наштовхне новий дублікат головного екрана на вершину стека. При спробі користувача натиснути системну кнопку «Назад» він не вийде із застосунку, а повернеться назад на щойно заповнену форму.
  • Рішення: Після завершення роботи з формою завжди викликати router.back() (або router.replace('/'), якщо повернення в історію неприпустиме).

5. Ігнорування колірних токенів теми для навігаційних панелей

  • Проблема: Застосунок підтримує темну тему, але панель вкладок залишається яскраво-білою за замовчуванням через відсутність конфігурації tabBarStyle та headerStyle.
  • Рішення: Обов'язково зв'язувати параметри навігаторів (tabBarStyle, headerStyle, tabBarActiveTintColor) із семантичними токенами дизайн-системи (colors.background, colors.border, colors.primary).

6. Вкладення модальних форм створення безпосередньо в список вкладок

  • Проблема: Додавання форми create-trip як четвертої постійної вкладки в нижню панель спотворює UX: форма не є рівноправним розділом застосунку, а таб-бар заважає введенню даних.
  • Рішення: Організовувати форми як окремі екрани кореневого стека Stack, що відкриваються поверх панелі вкладок.

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

Мета

Окремий застосунок поза 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
Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

title "Архітектура навігації міні-проєкту «Довідник міст»"

rectangle "app/_layout.tsx (Root Stack)" as R_STACK #dbeafe {
  
  rectangle "app/(tabs)/_layout.tsx (Tabs Navigator)" as T_NAV #e0e7ff {
    rectangle "app/(tabs)/index.tsx\n[Список міст]" as T_LIST #c7d2fe
    rectangle "app/(tabs)/about.tsx\n[Про проєкт]" as T_ABOUT #c7d2fe
  }
  
  rectangle "app/city/[id].tsx\n[Детальна картка міста]" as S_DETAIL #fef3c7
}

T_LIST -down-> S_DETAIL : <Link href={`/city/${city.id}`}>\n(Stack Push з кнопкою «Назад»)
S_DETAIL -up-> T_LIST : router.back()\n(Повернення до списку)
T_ABOUT .left.> T_LIST : <Link href="/"> (Перехід на список)
@enduml

Крок 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