Version2

Вкладені навігатори та модалки

Архітектура ієрархічних навігаційних дерев у Expo Router — вкладені стеки у вкладках (Nested Stack in Tabs), модальні представлення (presentation modal), сегрегація маршрутів через групи (auth), перехоплення переходів через usePreventRemove, міні-проєкт «Нотатки» та екран деталей поїздки в Nomad

Вкладені навігатори та модалки

Вступ: Еволюція навігаційного дерева до багаторівневих ієрархій

У попередньому розділі було розглянуто базові принципи функціонування Expo Router: базову структуру між каталогами app/ та src/, кореневий стековий навігатор Stack, панель нижніх вкладок Tabs на базі групи маршрутів (tabs), а також переходи через Link та useRouter. Створена структура задовольняла потреби базового інтерфейсу формату «три незалежні вкладки + один окремий екран форми».

Проте в реальних мобільних додатках простої лінійної структури швидко стає недостатньо. Більшість популярних додатків використовують складнішу поведінку:

  1. Контекстні переходи всередині розділу: Натискання на елемент списку у вкладці новин чи каталогу відкриває екран детального перегляду, при цьому нижня навігаційна панель залишається видимою та активною, дозволяючи користувачеві в будь-який момент перемкнутися на сусідній розділ і повернутися назад без втрати точки перегляду.
  2. Модальні контекстні сесії: Форми швидкого створення сутностей, фільтрації чи налаштувань з'являються поверх активного інтерфейсу у вигляді спливаючих карток (Modal Sheets), тимчасово перекриваючи вкладки та акцентуючи увагу користувача на завершенні конкретного завдання.
  3. Захист від випадкової втрати даних (Navigation Interception): Якщо користувач вніс зміни у форму й випадково ініціював жест повернення, навігаційна система перехоплює подію та запитує підтвердження на вихід.
  4. Розділення просторів екранів (Auth Flows): Екрани неавторизованої зони (вхід, реєстрація, відновлення доступу) функціонують в ізольованому навігаційному контурі, повністю відокремленому від робочої зони авторизованого користувача.

Даний розділ присвячений проєктуванню складених ієрархічних навігаційних систем:

  • Вкладені навігатори (Nested Navigators): конфігурація стекового контейнера Stack всередині конкретної вкладки Tabs.
  • Модальні представлення (Modal Presentations): відображення екранів у Stack у вигляді модальних вікон через параметр presentation: 'modal'.
  • Групи маршрутів для авторизації (auth): сегрегація публічних та захищених контекстів інтерфейсу.
  • Навігаційні гарди (Navigation Guards): перехоплення подій закриття екрана з незбереженими даними за допомогою хука usePreventRemove.

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

  • Зрозуміти структуру дерева навігаторів (Navigation Tree) і навчитися визначати, яка гілка графа повинна обробляти виклики router.push() та router.back().
  • Спроєктувати макет для потоку «список → деталі» зі збереженням видимості нижньої панелі вкладок (Bottom Tab Bar).
  • Налаштувати модальний режим показу presentation: 'modal' та освоїти параметри конфігурації платформних анімацій на iOS та Android.
  • Засвоїти механізм декларативного прив'язування початкового маршруту через конфігурацію unstable_settings.initialRouteName.
  • Реалізувати патерн надійного захисту форми від випадкового закриття (Dirty Form Guard) через хук usePreventRemove та системні діалоги Alert.
  • Побудувати автономний міні-проєкт «Нотатки» з вкладеним стеком і модалкою та масштабувати навігаційну архітектуру проєкту Nomad, додавши повноцінний екран деталей поїздки [id].tsx.
Головний принцип: У Expo Router та бібліотеці React Navigation навігаційна система являє собою не плаский масив маршрутів, а ієрархічне дерево контейнерів. Батьківський макет _layout.tsx обгортає дочірні сегменти, кожен з яких сам по собі може бути повноцінним автономним навігатором. Модальне вікно є лише способом візуального представлення екрана в стеку, а навігаційний гард — обробником події закриття екрана.
Платформові анімації та жести. Нативні жести інтерактивного змахування (Interactive Modal Dismiss), нативна смуга заголовка та переходи вкладок не емулюються у веб-пісочниці ::react-native-preview. Для валідації жестів використовуйте середовище Expo Go або нативний емулятор.

Репозиторій наскрізного проєкту Nomad: github.com/arakviel/nomad.


Чому простого плоского стека недостатньо

Розглянемо практичний сценарій, коли вся навігація застосунку організована виключно через єдиний кореневий стековий навігатор Root Stack без використання вкладених контейнерів усередині розділів.

       ПЛОСКИЙ ROOT STACK                             ВКЛАДЕНА ІЄРАРХІЯ (NESTED)
+------------------------------------+       +-----------------------------------------+
| Root Stack                         |       | Root Stack                              |
| ├── (tabs) [Поїздки, Місця, Ще]    |  vs   | ├── (tabs) [Tabs Layout]                |
| ├── trip-details.tsx  (Full Screen)|       | │   ├── trips/ [Вкладений Stack]        |
| └── create-trip.tsx   (Full Screen)|       | │   │   ├── index.tsx (Список)          |
+------------------------------------+       | │   │   └── [id].tsx (Деталі + Tabs!)   |
| • Вкладки зникають на деталях      |       | │   ├── places.tsx                      |
| • Втрата просторового контексту    |       | │   └── about.tsx                       |
| • Форма змішується зі стрічкою     |       | └── create-trip.tsx [Modal Presentation]|
+------------------------------------+       +-----------------------------------------+

Якщо розмістити екран деталей поїздки trip-details.tsx безпосередньо в кореневому стеку поруч із групою (tabs), перехід на деталі викличе наступні системні наслідки:

  1. Зникнення панелі вкладок: Кореневий стек накладає новий екран на всю площу дисплея поверх усього контейнера (tabs). Користувач втрачає можливість миттєвого переходу в розділ «Місця» або «Ще». Щоб переглянути збережені локації, йому доведеться спочатку повернутися назад, втративши відкритий контекст поїздки.
  2. Спотворення UX модальних операцій: Якщо форму створення create-trip розмістити як звичайний горизонтальний перехід усередині вкладки, користувач сприймає її як черговий рівень заглиблення контенту, а не як тимчасову сфокусовану транзакцію введення даних.
  3. Відсутність контролю за життєвим циклом введення: Випадковий свайп повернення на екрані редагування без навігаційного гарда призводить до миттєвого розмонтування компонента та безповоротної втрати набраного тексту в оперативній пам'яті.

Вирішення цих проблем вимагає комбінування трьох інструментів: вкладених навігаторів, модальних представлень та гардінг-хуків.


Теорія вкладених навігаторів (Nested Navigators)

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

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

Структура навігаційного дерева проєкту Nomad:

                            ГЛОБАЛЬНЕ ДЕРЕВО НАВІГАЦІЇ
+---------------------------------------------------------------------------------+
| Root Stack Navigator (app/_layout.tsx)                                          |
|                                                                                 |
|  +---------------------------------------------------------------------------+  |
|  | (tabs) Bottom Tabs Navigator (app/(tabs)/_layout.tsx)                     |  |
|  |                                                                           |  |
|  |  +---------------------------------------------------------------------+  |  |
|  |  | trips/ Nested Stack Navigator (app/(tabs)/trips/_layout.tsx)        |  |  |
|  |  |                                                                     |  |  |
|  |  |  [index.tsx (Стрічка)]  ==== push ====>  [[id].tsx (Деталі поїздки)]|  |  |
|  |  +---------------------------------------------------------------------+  |  |
|  |                                                                           |  |
|  |  [places.tsx (Вкладка «Місця»)]     [about.tsx (Вкладка «Ще»)]            |  |
|  +---------------------------------------------------------------------------+  |
|                                                                                 |
|  +---------------------------------------------------------------------------+  |
|  | create-trip.tsx (Root Stack Screen, presentation: 'modal')                |  |
|  +---------------------------------------------------------------------------+  |
+---------------------------------------------------------------------------------+
  • Рівень 1 (Кореневий Stack app/_layout.tsx): Керує двома основними просторами: глобальним контейнером вкладок (tabs) та окремим модальним екраном створення create-trip.
  • Рівень 2 (Контейнер вкладок app/(tabs)/_layout.tsx): Організовує три паралельні розділи застосунку: trips, places та about.
  • Рівень 3 (Вкладений Stack app/(tabs)/trips/_layout.tsx): Функціонує виключно всередині вкладки «Поїздки», ізольовано керуючи переходами між стрічкою index.tsx та карткою деталей [id].tsx.

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

rectangle "Root Stack\n(app/_layout.tsx)" as R {
  rectangle "(tabs) Tabs\n(app/(tabs)/_layout.tsx)" as T {
    rectangle "trips Stack\n(app/(tabs)/trips/_layout.tsx)" as S {
      rectangle "index\nсписок" as I
      rectangle "[id]\nдеталі" as D
    }
    rectangle "places" as P
    rectangle "about" as A
  }
  rectangle "create-trip\npresentation: modal" as M
}

I -down-> D : push у вкладеному Stack
T -right-> M : push у root Stack

note right of D
  Tab bar зазвичай
  лишається видимим
end note

note right of M
  Tabs під модалкою
  (інший шар UI)
end note

@enduml

Архітектурний вибір: Вкладений Detail проти Кореневого Detail

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

Критерій порівнянняВкладений стек: app/(tabs)/trips/[id].tsxКореневий стек: app/trips/[id].tsx
Видимість Tab BarПанель вкладок залишається видимою під час перегляду деталей.Панель вкладок повністю приховується.
UX-призначенняДослідження контенту як природного розширення поточної вкладки.Повне занурення в контент (відеоплеєр, детальна інтерактивна карта, фотогалерея).
Поведінка при зміні вкладкиСтан перегляду зберігається у вкладці при перемиканні туди й назад.Користувач не бачить вкладок; для зміни розділу зобов'язаний закрити екран.
Багатоточкова навігаціяВідкриття з інших вкладок перемикає активну вкладку на trips.Може викликатися як універсальний оверлей із будь-якої точки застосунку.

Для проєкту Nomad обрано архітектуру вкладеного стека: щоденник подорожей передбачає часте зіставлення опису поїздки зі збереженими локаціями у вкладці «Місця», тому збереження таб-бару на екрані деталей є критичною вимогою до зручності використання.


Реалізація вкладеного Stack усередині вкладки

Трансформація файлової структури

Для перетворення простої вкладки на повноцінний вкладений стек ми замінюємо окремий файл app/(tabs)/index.tsx на підкаталог app/(tabs)/trips/, що містить власний файл макета та маршрути:

# Структура статті 11 (Плоска вкладка)
app/(tabs)/
  index.tsx             ← Головний екран (неможливо додати вкладений стек)
  places.tsx
  about.tsx

# Структура статті 12 (Вкладений стек)
app/(tabs)/
  trips/
    _layout.tsx         ← Локальний Stack тільки для розділу «Поїздки»
    index.tsx           ← Маршрут "/trips" (Стрічка поїздок)
    [id].tsx            ← Маршрут "/trips/:id" (Деталі конкретної поїздки)
  places.tsx            ← Маршрут "/places"
  about.tsx             ← Маршрут "/about"

Координація макетів та сегрегація обов'язків

Tabs Layout (app/(tabs)/_layout.tsx)
батьківський навігатор
Реєструє лише верхньорівневі вкладки (trips, places, about). Для таб-макета сегмент trips є єдиним неподільним екраном; він не володіє інформацією про внутрішню структуру маршрутів trips/[id].
Trips Stack Layout (app/(tabs)/trips/_layout.tsx)
вкладений навігатор
Керує екранами гілки подорожей: формує нативний заголовок для деталей, вимикає заголовок для списку (headerShown: false) та задає анімації переходу всередині вкладки.
Root Stack Layout (app/_layout.tsx)
кореневий навігатор
Оперує глобальними просторами: контейнером (tabs) як єдиним вузлом та модальним вікном create-trip.

Лістинг вкладеного макета: app/(tabs)/trips/_layout.tsx

import { Stack } from 'expo-router';
import { useTheme } from '@/shared/theme';

export default function TripsStackLayout() {
  const { colors } = useTheme();

  return (
    <Stack
      screenOptions={{
        headerStyle: { backgroundColor: colors.background },
        headerTintColor: colors.primary,
        headerTitleStyle: { color: colors.text, fontWeight: '600' },
        headerShadowVisible: false,
        contentStyle: { backgroundColor: colors.background },
      }}
    >
      {/* Стрічка має власний кастомний заголовок — нативний header вимикаємо */}
      <Stack.Screen name="index" options={{ headerShown: false }} />
      
      {/* Екран деталей використовує системний header із кнопкою повернення */}
      <Stack.Screen
        name="[id]"
        options={{
          title: 'Поїздка',
          headerBackTitle: 'Список',
        }}
      />
    </Stack>
  );
}

Як працює обробник router.back()

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

  1. Якщо користувач перебуває на маршруті /trips/42 (вкладений стек trips), виклик router.back() знімає екран деталей і повертає до списку /trips. Нижня панель вкладок не змінює свого стану.
  2. Якщо користувач перебуває на маршруті /create-trip (кореневий стек), виклик router.back() закриває модальне вікно та повертає користувача в ту точку графа, звідки було ініційовано відкриття (наприклад, назад на екран деталей або в список).

Модальні представлення (Modal Presentations)

Семантична різниця між Push та Modal

У мобільному UX розрізняють два принципово різні типи переходів:

  • Card Push (Ієрархічний перехід): переміщення вглиб структури даних. Екран виїжджає справа наліво, сигналізуючи про перехід на нижчий рівень поточної ієрархії.
  • Modal Presentation (Модальне представлення): переривання основного робочого потоку для виконання ізольованої транзакції (створення нотатки, встановлення фільтра, перегляд фото). Екран з'являється знизу вгору, частково або повністю перекриваючи контекст.

Конфігурація модального екрана в Expo Router

В Expo Router модальне вікно не вимагає окремого фреймворку чи зовнішніх бібліотек. Це той самий звичайний файл маршруту в app/, для якого в батьківському Stack встановлено властивість presentation: 'modal'.

// app/_layout.tsx
<Stack.Screen
  name="create-trip"
  options={{
    presentation: 'modal',
    title: 'Нова поїздка',
    headerShown: true,
    headerBackTitle: 'Закрити',
  }}
/>

Платформові відмінності модальних вікон

На платформі iOS значення presentation: 'modal' транслюється у нативну модель спливаючої картки, яка залишає видимим затемнений верхній край попереднього екрана. Підтримується нативний жест змахування вниз для закриття (Interactive Pull-Down Dismiss).

Конфігурація якоря початкового маршруту (unstable_settings)

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

Для запобігання цьому в кореневому макеті експортується об'єкт конфігурації unstable_settings:

export const unstable_settings = {
  initialRouteName: '(tabs)',
};

Цей параметр гарантує, що рушій Expo Router попередньо змонтує групу (tabs) як фундамент навігаційного стека, навіть якщо цільовий запуск відбувся безпосередньо на модальний маршрут /create-trip.


Архітектура авторизаційних потоків: Групи (auth)

У реальних мобільних системах застосунок розділений на взаємовиключні функціональні домени: публічний простір неавторизованого гостя та приватний робочий простір авторизованого користувача.

Організація каталогів за допомогою груп

app/
  _layout.tsx           ← Кореневий перемикач контекстів (Redirect / Guard)
  (auth)/               ← Публічний домен авторизації
    _layout.tsx         ← Стек без нижніх вкладок
    login.tsx           ← Маршрут "/login"
    register.tsx        ← Маршрут "/register"
  (app)/                ← Захищений приватний домен
    (tabs)/
      _layout.tsx       ← Вкладки застосунку

Принцип роботи декларативного захисту

  1. Ізоляція макетів: Група (auth) має власний файл макета _layout.tsx, який не містить контейнера Tabs. Завдяки цьому екран авторизації завжди відображається на весь екран без панелі вкладок.
  2. Атомарне очищення історії: Після успішного входу в систему перехід у робочий простір здійснюється виключно через метод router.replace('/(app)/(tabs)'). Це повністю очищає стекову історію екранів вводу логіна та пароля, унеможливлюючи повернення назад системною кнопкою.
Безпека даних проти UI-маршрутизації. Розподіл екранів на групи (auth) та (app) вирішує виключно завдання інтерфейсної ергономіки та порядку показу вікон. Приховування вкладки не захищає дані від витоку. Справжня безпека системи забезпечується валідацією токенів (JWT) на бекенд-сервері та криптографічним зберіганням ключів у нативному сховищі expo-secure-store.

Навігаційні гарди: Запобігання втраті даних через usePreventRemove

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

Життєвий цикл перехоплення навігації

Для реалізації патерну захисту використовується хук usePreventRemove з екосистеми React Navigation:

import { useState } from 'react';
import { Alert } from 'react-native';
import { useNavigation } from 'expo-router';
import { usePreventRemove } from '@react-navigation/native';

export function FormScreen() {
  const navigation = useNavigation();
  const [text, setText] = useState('');
  const [isSaved, setIsSaved] = useState(false);

  // Стан «забрудненості» форми (Dirty State)
  const isDirty = !isSaved && text.trim().length > 0;

  usePreventRemove(isDirty, ({ data }) => {
    // Викликається тільки тоді, коли isDirty === true під час спроби навігаційного виходу
    Alert.alert(
      'Незбережені зміни',
      'Якщо ви вийдете зараз, усі внесені дані будуть втрачені. Ви дійсно бажаєте вийти?',
      [
        { text: 'Залишитися', style: 'cancel' },
        {
          text: 'Вийти без збереження',
          style: 'destructive',
          // Відновлення перерваної навігаційної дії
          onPress: () => navigation.dispatch(data.action),
        },
      ],
    );
  });

  return (/* JSX розмітка форми */);
}

Покрокова механіка функціонування:

  1. Користувач вводить текст у поле форми. Значення стану isDirty переходить у true.
  2. Користувач виконує жест змахування вниз або натискає системну кнопку «Назад».
  3. Навігаційний рушій перехоплює намір демонтувати екран і передає керування колбеку usePreventRemove.
  4. Якщо користувач обирає варіант «Залишитися», навігаційна дія відхиляється, і екран залишається у фокусі.
  5. Якщо користувач підтверджує вихід, викликається метод navigation.dispatch(data.action), який примусово виконує відкладену дію закриття.

Динамічні сегменти маршруту: [id].tsx

Файл з назвою app/(tabs)/trips/[id].tsx декларує параметризований динамічний маршрут, де сегмент [id] виступає змінною частиною URL:

import { useLocalSearchParams } from 'expo-router';
import { getTripById } from '@/features/trips';

export default function TripDetailsScreen() {
  // Вилучення динамічного параметра з URL
  const { id } = useLocalSearchParams<{ id: string }>();
  
  // Нормалізація параметра (захист від випадку string[])
  const tripId = Array.isArray(id) ? id[0] : id;
  const trip = tripId ? getTripById(tripId) : undefined;

  if (!trip) {
    return <NotFoundView message={`Поїздку з ID ${tripId} не знайдено`} />;
  }

  return <TripDetailsView trip={trip} />;
}

Хук useLocalSearchParams здійснює десеріалізацію параметрів поточного маршруту. Нормалізація через Array.isArray(id) є рекомендованою практикою для гарантування строгих типів даних у TypeScript.


Комплексний огляд навігаційних потоків Nomad

Розглянемо наскрізний ланцюг взаємодії користувача з оновленою архітектурою Nomad:

1. Перегляд стрічки у вкладці «Поїздки»

Активним є контейнер Tabs. Контент рендериться з маршруту (tabs)/trips/index.tsx усередині вкладеного стека. Нижній таб-бар видимий.

2. Вибір поїздки для детального ознайомлення

Виклик router.push('/trips/1') ініціює операцію push у внутрішньому стеку вкладки. Екран плавно зсувається, відображаючи нативний заголовок з назвою поїздки. Нижня панель вкладок залишається видимою.

3. Відкриття форми створення поїздки

Натискання кнопки «Нова поїздка» викликає router.push('/create-trip'). Оскільки цей маршрут зареєстрований у кореневому стеку з опцією presentation: 'modal', форма виїжджає знизу на весь екран, перекриваючи таб-бар.

4. Збереження форми та синхронізація стану

Після валідації форма викликає метод addTrip(newTrip) глобального контексту TripsProvider та ініціює виклик router.back(). Модальне вікно закривається, повертаючи користувача назад на екран деталей або стрічку, де нова картка автоматично з'являється завдяки підписці на React Context.

5. Перехід із вкладки «Місця» на поїздку

Натискання на чіп збереженої локації у вкладці «Місця» викликає router.push('/trips/' + place.tripId). Навігатор автоматично активує вкладку «Поїздки» та відкриває відповідний екран деталей у вкладеному стеку.


Архітектурні антипатерни у складених навігаційних системах

1. Винесення деталей у кореневий стек без усвідомленої мети приховати Tab Bar

  • Симптом: Користувач скаржиться, що під час перегляду деталей неможливо швидко перемкнутися на сусідню вкладку.
  • Причина: Файл деталей помилково розміщено в app/trips/[id].tsx замість вкладеної структури app/(tabs)/trips/[id].tsx.

2. Реєстрація модальної форми як постійної вкладки в Tab Bar

  • Симптом: Форма створення постійно присутня в нижній панелі як окрема іконка, порушуючи модель персистентних розділів.
  • Причина: Нерозуміння різниці між довгоживучими розділами (Tabs) та тимчасовими контекстними операціями (Modal).

3. Спроба закриття модалки через виклик router.push('/')

  • Симптом: Навігаційний стек роздувається дублікатами домашнього екрана, а подальше натискання системної кнопки «Назад» повертає користувача назад на заповнену форму.
  • Причина: Використання операції push замість обов'язкового виклику router.back() або router.replace().

4. Відсутність навігаційного гарда на формі з великим обсягом вводу

  • Симптом: Випадковий дотик до краю дисплея на iOS викликає закриття модалки та повну втрату введеного тексту.
  • Причина: Нехтування хуком usePreventRemove під час проєктування критичних інтерфейсів введення.

5. Розміщення важкої бізнес-логіки безпосередньо у файлах макетів _layout.tsx

  • Симптом: Повторні небажані мережеві запити та падіння продуктивності при переході між екранами.
  • Причина: Файл макета повинен займатися виключно зв'язуванням навігаційних контейнерів та конфігурацією опцій, делегуючи отримання даних спеціалізованим хукам та контекстним провайдерам.

Міні-проєкт: «Нотатки» (від А до Я)

Мета

Окремий застосунок поза Nomad. Увесь код — у collapsible нижче.

  1. Tabs: Нотатки | Про застосунок.
  2. У «Нотатки» — nested Stack: список → деталі.
  3. Modal «Нова нотатка» + guard на back.
  4. Збережені нотатки з’являються в списку.

Структура

notes-app/
  app/
    _layout.tsx
    (tabs)/
      _layout.tsx
      notes/
        _layout.tsx
        index.tsx
        [id].tsx
      about.tsx
    modal/
      new.tsx
  src/data/notesStore.ts

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

1. Скарфолд

npx create-expo-app@latest notes-app -t tabs
cd notes-app

2. Іконки (за потреби)

npx expo install @expo/vector-icons

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

npx expo start

Видаліть зайві екрани шаблону (explore тощо), залиште структуру як вище.

Крок 2. src/data/notesStore.ts

Крок 3. app/_layout.tsx

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

Крок 5. Nested stack — app/(tabs)/notes/_layout.tsx

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

Крок 7. Деталі — app/(tabs)/notes/[id].tsx

Крок 8. Modal + guard — app/modal/new.tsx

Крок 9. About

Перевірка міні-проєкту

  1. Список нотаток + вкладка «Про застосунок».
  2. Тап по нотатці → деталі, tab bar лишається, back → список.
  3. «Нова нотатка» → modal.
  4. Ввести текст, натиснути back → діалог. «Лишитись» / «Вийти».
  5. «Зберегти» → modal закривається, нотатка в списку.

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

  • Усі файли з повним кодом підставлено
  • Nested Stack у notes/
  • Modal modal/new
  • Guard на dirty
  • Немає useState('screen') замість Router

Nomad: стек деталей поїздки та модалки

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

  • Тап по картці поїздки відкриває деталі (опис, мітки, місця поїздки).
  • Tab bar лишається — можна перейти в «Місця» / «Ще».
  • «Нова поїздка» відкривається як modal (з форми на home, з about Link, з деталей).
  • Місця з вкладки «Місця» ведуть на деталі відповідної поїздки.

Нитка

Уже є: tabs Поїздки/Місця/Ще, форма RHF, TripsProvider, тема, FlashList.

Додаємо:

ЗмінаНавіщо
(tabs)/trips/ + nested Stackсписок + [id]
getTrip, getPlacesForTripдані деталей
create-trippresentation: 'modal'модальна форма
unstable_settings.initialRouteNameякір для modal
навігація з places / chipsу деталі поїздки

Повний знімок

Перевірка Nomad

  1. Вкладка «Поїздки» — список як раніше.
  2. Тап по картці → деталі, tabs знизу. Back → список.
  3. «Нова поїздка» → modal поверх. Зберегти / скасувати → dismiss.
  4. З деталей кнопка «Нова поїздка (modal)» теж відкриває modal.
  5. Вкладка «Місця» → тап → деталі поїздки цього місця.
  6. Тема / форма / списки не зламані.

Коміт

cd /path/to/nomad
git add -A
git commit -m "$(cat <<'EOF'
feat: trip details stack and modals

Material: content/15.react-native/12.nested-navigators-and-modals.md
EOF
)"
git push

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

Базовий

  1. Своїми словами: card vs modal.
  2. Чому tabs лишаються на /trips/1, але ховаються під create-trip modal.
  3. Навіщо initialRouteName: '(tabs)'.

Середній

  1. Міні-проєкт «Нотатки» повністю.
  2. У Nomad додати headerRight на деталях — кнопка «Поділитись» (поки Alert).
  3. Guard на create-trip, якщо formState.isDirty.

Професійний

  1. Порівняти detail у nested stack vs detail на root Stack (tabs hide) — короткий висновок UX.
  2. Нарис структури (auth) + Stack.Protected без повного SecureStore.
  3. presentation: 'formSheet' на iOS (якщо SDK дозволяє) — нотатка з відмінностей.

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


Що далі

Вкладеність і modal закривають «геометрію» навігації. Далі — deep linking і params: параметри маршруту, scheme, universal links і cold start.

Expo Router nested · Modals · Common patterns · Nomad

Copyright © 2026