React Native

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

Stack усередині Tabs, presentation modal, групи auth, guard на back — нотатки з модалкою і деталі поїздки в Nomad

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

З чого почати

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

Але варто відкрити майже будь-який зрілий застосунок на телефоні — і картина складніша. На вкладці «Стрічка» тап по картці відкриває деталі, при цьому нижня панель вкладок часто лишається (або зникає — залежно від продукту). Форма «щось швидко додати» виїжджає зверху / як sheet, а не «вклеюється» в ту саму історію, що й глибокий перегляд. Якщо користувач набрав текст і випадково свайпнув «назад», з’являється питання: «Вийти без збереження?» Окремо існує «світ гостя» (вхід / реєстрація) і «світ авторизованого користувача» (вкладки, профіль) — це вже не один плоский список екранів.

Ця стаття якраз про другий поверх навігації:

  1. Вкладені навігатори (nested navigators) — Stack усередині вкладки Tabs.
  2. Модальні екрани — той самий Stack, але з іншим presentation.
  3. Групи маршрутів на кшталт (auth) — організація «різних світів» UI.
  4. Guard на back — перехоплення виходу з екрана з незбереженими даними.

Глибокий розбір query-параметрів, cold start і universal links лишаємо наступній статті. Динамічний сегмент [id] тут з’являється лише в обсязі, потрібному для екрана деталей.

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

  1. Пояснити людською мовою, навіщо вкладати Stack у Tabs, а не тримати все в одному стеку.
  2. Намалювати на папері дерево layout для list → detail зі збереженим tab bar.
  3. Налаштувати presentation: 'modal' і пояснити, чим це відрізняється від звичайного push.
  4. Сказати, який саме стек реагує на router.back() у різних ситуаціях.
  5. Описати ідею груп (auth) / «захищених» маршрутів без плутанини з безпекою API.
  6. Реалізувати guard «форма брудна → підтвердження».
  7. Зібрати міні-проєкт «Нотатки» за повним кодом і перенести ідеї в Nomad.
Головна думка. У Expo Router (і в React Navigation під ним) навігатори — це не «один глобальний список екранів», а дерево. Батьківський layout обгортає дітей; дитина може бути ще одним навігатором. Modal — це переважно спосіб показати екран у Stack, а не окрема бібліотека. Guard на back — шар поведінки при leave, поверх геометрії маршрутів.
Живі жести, tab bar і модальна анімація не відтворюються в ::react-native-preview на сайті. У статті — послідовне пояснення, схеми й код; перевірка — у Expo Go або симуляторі.

Код наскрізного застосунку: github.com/arakviel/nomad.


Життєва сцена: що ламається без вкладеності

Уявіть Nomad лише з «плоским» root Stack і без вкладеного стека у «Поїздках».

Користувач на стрічці тапає «Карпати на вихідні». Якщо деталі покласти на root Stack поруч із вкладками, часто виходить так: екран деталей відкривається на весь застосунок, нижня панель зникає. Це нормальний і навіть бажаний UX для деяких продуктів (читалка, відео). Але для щоденника подорожей часто хочуть інше: «я всередині розділу Поїздки, можу швидко глянути Місця, не гублячи контекст».

Інший провал — навпаки: форму створення теж запхати всередину вкладки як звичайний push. Тоді «Нова поїздка» виглядає як ще один «глибокий» екран розділу, tab bar може лишатись під довгою формою, а жест «закрити задачу» менш очевидний, ніж у модалки «відкрив → зберіг / закрив».

Третій провал — ігнорувати історію. Користувач відкрив форму, почав друкувати, випадково свайпнув dismiss — і все зникло. Людина не каже «не було usePreventRemove»; каже «додаток з’їв мою нотатку».

Отже, тема статті — не «ще три API», а три різні задачі UX, які розв’язують вкладеністю, modal і guard.


Що таке «вкладений навігатор» простими словами

Навігатор (navigator) — компонент, який вирішує, який із набору екранів зараз видимий і як між ними перемикатися (стопка, вкладки, drawer…).

У статті 11 ми вже мали:

  • Tabs — кілька розділів «поруч»;
  • Stack — екрани «один на одному» з історією back.

Вкладений навігатор (nested navigator) — ситуація, коли один навігатор є екраном (або гілкою) іншого.

Приклад, який ми збираємо в Nomad:

  1. КоріньStack (файл app/_layout.tsx).
  2. Один із «екранів» кореня — ціла група вкладок (tabs) (усередині — Tabs).
  3. Одна з вкладок — не «просто сторінка», а знову Stack (тека trips/ зі своїм _layout.tsx).
  4. У цьому внутрішньому Stack: index (список) і [id] (деталі).
  5. Окремо на кореневому Stack — create-trip з presentation: 'modal'.

Користувач цього дерева «не бачить». Він бачить: вкладки, стрічку, деталі, модалку. Але розробник зобов’язаний тримати дерево в голові: інакше router.back() «не туди», tab bar «зник», modal «не закривається».

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

Навіщо не тримати «все в одному Stack»

Можна уявити радикально простий варіант: один кореневий Stack і жодних Tabs — Home, TripDetail, Places, About, Create, Login як послідовні екрани. Технічно це працює. Продуктово — ні, щойно з’являються постійні розділи.

Типові болі «плоского» стека:

  1. Немає «дому» для розділу. Користувач звик, що «Поїздки / Місця / Ще» завжди внизу.
  2. Back плутає розділи. З «Місць» back може вийти на «деталі поїздки», хоча людина вже «змінила тему».
  3. Довга історія. Десять push підряд — і системний back довго «відмотує» те, чого користувач уже не пам’ятає.
  4. Складно зберігати стан розділу. Вкладки як раз і дають паралельні «гілки» з власною (вкладеною) історією.

Tabs розв’язують паралельність.
Stack усередині вкладки розв’язує глибину в одній гілці.

Це не догма. Іноді деталі свідомо виносять на root Stack, щоб сховати tab bar і дати більше місця. Важливо обрати свідомо, а не отримати побічний ефект «так вийшло з файлами».


Місток зі статті 11 (не таблицею «для своїх»)

Після статті 11 у вас уже є:

  • розуміння app/, _layout.tsx, index, груп (tabs);
  • Tabs і root Stack;
  • Link, router.push, router.back;
  • уявлення про typed routes.

Що змінюється зараз — глибина дерева, а не «інший Router».

Було (типово після ст. 11)Стає (ця стаття)
Вкладка = один файл (index.tsx)Вкладка = тека з власним Stack (trips/)
Усі «другі екрани» на rootДеталі — в гілці вкладки; modal — на root
create-trip як звичайний cardcreate-trip як modal
back «просто назад»Питання: назад у якому стеку?
Auth «колись потім»Карта груп (auth) / app (без повного login у Nomad)
Guard на leave з брудної форми

Як влаштований вкладений Stack у вкладці

Від одного файлу до каталогу

У статті 11 вкладка «Поїздки» могла бути просто app/(tabs)/index.tsx. Цього мало, щойно з’являється другий екран того ж розділу.

Трансформація:

# було
app/(tabs)/
  index.tsx          ← і список, і «куди ж діти деталі?»

# стало
app/(tabs)/
  trips/
    _layout.tsx      ← Stack тільки для гілки «Поїздки»
    index.tsx        ← список  → маршрут /trips
    [id].tsx         ← деталі → маршрут /trips/42
  places.tsx
  about.tsx

У app/(tabs)/_layout.tsx замість name="index" з’являється name="trips" — бо вкладка тепер тека, а не файл.

Що відчуває користувач

  1. Відкриває вкладку «Поїздки» — бачить список (як раніше).
  2. Тапає картку — анімація push всередині розділу; внизу лишаються Поїздки / Місця / Ще.
  3. Тисне «назад» (header або жест) — знову список цієї вкладки, а не «виліт» із розділу.
  4. Перемикає на «Місця» — інший розділ; повернувшись на «Поїздки», часто знову бачить той самий вкладений стан (залежить від того, чи екран розмонтували — але ідея гілки саме така).

Порівняйте з modal «Нова поїздка»: там вкладки під модалкою, задача відчувається як оверлей, не як «глибше в список».

Коли обирати nested detail, а коли root detail

Nested Stack (tabs видно) доречний, коли:

  • деталі — природне продовження стрічки того ж розділу;
  • користувач часто стрибає між вкладками;
  • не потрібен «кіношний» повноекранний режим.

Detail на root Stack (tabs ховаються) доречний, коли:

  • потрібна максимальна площа (медіа, карта на весь екран);
  • flow «зайшов у деталі = тимчасово вийшов з tab-світу»;
  • той самий detail відкривають з кількох вкладок і хочуть одну повноекранну реалізацію.

У Nomad для поїздок обрано nested: щоденник — це розділ зі стрічкою й карткою, tabs корисні поруч. Форма створення — modal на root: окрема задача, не ще один рядок стрічки.

Мінімальний код вкладеного layout

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

export default function TripsStackLayout() {
  return (
    <Stack>
      <Stack.Screen name="index" options={{ headerShown: false }} />
      <Stack.Screen
        name="[id]"
        options={{ title: 'Поїздка', headerBackTitle: 'Список' }}
      />
    </Stack>
  );
}

Список відкриває деталі:

router.push(`/trips/${trip.id}`);
// або <Link href={`/trips/${trip.id}`} asChild>…

Зверніть увагу на шлях: /trips/..., бо сегмент теки trips входить у URL (на відміну від групи (tabs), яка в URL не видно).

Хто ким керує

Tabs layout
app/(tabs)/_layout.tsx
Знає лише вкладки: trips, places, about. Він не перелічує екрани trips/[id] — для нього вкладка trips — «один слот», усередині якого вже живе свій навігатор.
Вкладений Stack layout
app/(tabs)/trips/_layout.tsx
Знає екрани гілки поїздок: index і [id]. Саме він малює header деталей і обробляє back зі списку на деталі й назад.
Root Stack layout
app/_layout.tsx
Знає «великі» гілки: (tabs) як ціле і, наприклад, create-trip як modal. Root не займається push між списком і деталями поїздки — це робота nested Stack.
headerShown: false на index
options
Список часто з власним заголовком у контенті (як у Nomad: «Мандрівник», чіпи теми). Системний header там зайвий. На деталях header, навпаки, зручний: title + системна «назад».
headerBackTitle
string (особливо iOS)
Текст біля стрілки «назад» на iOS. «Список» або «Поїздки» підказує, куди повернетесь — важливіше, ніж здається на демо з одним екраном.

Який router.back() спрацьовує

Це місце, де «суха» документація найчастіше підводить.

  • Стоїте на /trips/3 (деталі у nested Stack) → router.back() зазвичай веде на /trips (список). Tab bar як був.
  • Стоїте на /create-trip (modal на root) → router.back() закриває modal і повертає туди, звідки відкрили (список, about, навіть деталі).
  • Якщо зробити router.push('/') «аби як закрити» після форми — можете накласти зайвий екран в історію; back потім відкриє порожню форму знову. Після modal майже завжди back() або replace, не «push home».

Правило: back знімає верхівку того стека, у якому цей екран лежить. Nested detail і root modal — різні стеки.


Що таке modal у термінах UX

Модальний екран (modal screen) — UI, який перериває поточний контекст і вимагає дії (заповнити, підтвердити, закрити), після чого людина повертається туди, де була.

Приклади з життя телефону: «Новий контакт», «Фільтри», «Поділитись», «Швидка нотатка».

Це відрізняється від:

  • вкладки — довгоживучий розділ;
  • detail push — поглиблення в контент того ж розділу.

Що таке modal у термінах Expo Router / React Navigation

У Stack у кожного екрана є опції показу. Ключова — presentation:

  • card (часто за замовчуванням) — «звичайний» перехід у стос екранів;
  • modal — модальний стиль (на iOS часто card-over-card / sheet-подібна метафора; на Android — свій material-перехід).

Тобто файл create-trip.tsxтой самий тип маршруту, що й інші. Відрізняється options на Stack.Screen у layout.

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

Відкриття не потребує окремого API:

router.push('/create-trip');

Закриття:

router.back(); // після submit або «Скасувати»

Де фізично лежить файл modal

Практичне правило курсу:

  • (tabs)/... — довгоживучі розділи й їхня внутрішня глибина;
  • поруч із (tabs) на root Stack — modal і інші «оверлейні» задачі.

Якщо покласти modal всередину trips/ Stack, він стане частиною історії вкладки. Інколи це потрібно; для «Нова поїздка» в Nomad зручніше root: форма одна, відкривається з home, about, деталей — і завжди виглядає як глобальна задача.

unstable_settings і якір

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

Навіщо: якщо застосунок перезавантажили або відкрили deep link одразу на modal, навігатору потрібен якір — базовий екран, «від якого» існує історія. Інакше кнопка «закрити» може повести себе дивно (немає куди back). Назва unstable_settings у Expo — історична; опція широко використовується. Слідкуйте за docs вашого SDK.

Після submit: хто оновлює список

Modal не «повертає props» батькові як React-колбек між деревами файлів. У Nomad список живе в TripsProvider (Context) вище за Stack. Форма викликає addTrip(trip) і router.back(). Вкладка зі списком уже підписана на той самий Context — картка з’являється без «перезавантаження маршруту».

Це важливий архітектурний урок: стан, спільний для кількох гілок навігації, піднімають над навігаторами (Context зараз, RTK пізніше).

iOS і Android

Modal часто асоціюється з карткою, що наїжджає, інколи зі свайпом dismiss. Підпис у header («Закрити» / back title) варто задати явно — дефолт може бути англійським або невдалим. Перевіряйте жест dismiss: якщо форма dirty, знадобиться guard (див. нижче), інакше свайп тихо знищить введене.
presentation
'card' \| 'modal' \| 'transparentModal' \| …
Головний важіль «як виглядає push». Для курсу й Nomad достатньо пари card vs modal. Існують також formSheet та інші режими — вони дають sheet зі snap points; підключайте, коли вже стабільні базові modal, і обов’язково перевіряйте на реальних iOS/Android вашого SDK.
файл create-trip.tsx
маршрут
Звичайний екран із default export. Modal-поведінка задається не ім’ям файлу, а Stack.Screen options у layout (хоча ім’я файлу = name маршруту).
header на modal
options
Часто headerShown: true, title «Нова поїздка», back/close. Можна сховати header і намалювати свій — тоді не забудьте доступний спосіб закрити (кнопка + Back).
провайдери
архітектура
ThemeProvider / TripsProvider мають обгортати root Stack, а не лише (tabs). Інакше modal «не бачить» useTrips() або тему.

Групи (auth): два світи інтерфейсу

У Nomad повноцінний login ще попереду (SecureStore, сесія). Але картографію маршрутів закладають рано, щоб потім не переписувати все дерево.

Проблема

Екрани «Увійти» / «Зареєструватись» не повинні сидіти всередині (tabs) як «ще одна вкладка». І навпаки: стрічку поїздок не показують гостю так само, як користувачу, який увійшов (або показують, але з іншим guard — залежить від продукту).

Ідея груп

Використовують групи тек (дужки, як (tabs)):

app/
  _layout.tsx
  (auth)/
    login.tsx
    register.tsx
  (app)/          # або одразу (tabs)
    (tabs)/...

Група не додає сегмент у URL, але дозволяє:

  • окремі layout (без tab bar на логіні);
  • умовний редірект: «немає сесії → login»;
  • replace після успішного входу, щоб Back не повернув на форму пароля.

Stack.Protected і класичний редірект

У новіших версіях Expo Router з’являється декларативний Stack.Protected з guard={isLoggedIn} — екрани всередині рендеряться / доступні лише коли guard істинний. Класичний підхід: у root layout читаєте сесію і робите router.replace('/login') або replace('/(tabs)').

Обидва підходи — про UI-маршрути. Вони не замінюють:

  • перевірку JWT на сервері;
  • secure storage;
  • захист API.
Часта плутанина. «Ми сховали вкладки, доки немає логіну» ≠ «дані захищені». Зловмисник не зобов’язаний користуватись вашим UI. Auth-групи — UX і порядок екранів; безпека — на бекенді й у правильному зберіганні сесії (окремі статті курсу).

У цій статті для Nomad повний (auth) не впроваджуємо — лише закладаємо розуміння. Міні-проєкт зосереджений на nested + modal + guard.


Guard на «назад»: не втратити введене

Сценарій

Користувач відкрив modal «Нова нотатка», написав абзац, випадково:

  • свайпнув dismiss на iOS;
  • натиснув системний Back на Android;
  • тапнув «закрити» в header.

Без додаткової логіки екран просто знімається, текст зникає. Це не «баг React», а відсутність продуктового правила.

Терміни

Dirty (брудна форма) — користувач уже щось змінив порівняно з початковим станом. У React Hook Form це formState.isDirty. У простому useState — наприклад title.trim() || body.trim().

Leave / remove — подія «цей екран хочуть прибрати з навігаційної історії» (back, dismiss, replace тощо).

Guard — логіка «не прибирати одразу, а спитати / перевірити».

usePreventRemove

У React Navigation (Expo Router стоїть на ньому) є хук usePreventRemove з пакета @react-navigation/native.

Ідея API:

usePreventRemove(dirty, ({ data }) => {
  // dirty === true → сюди потрапляємо замість миттєвого leave
  Alert.alert('Вийти без збереження?', '…', [
    { text: 'Лишитись', style: 'cancel' },
    {
      text: 'Вийти',
      style: 'destructive',
      onPress: () => navigation.dispatch(data.action),
    },
  ]);
});

Покроково:

  1. Користувач ініціює back/dismiss.
  2. Якщо dirty === false — хук не блокує; екран іде як завжди.
  3. Якщо dirty === true — навігація зупиняється, викликається callback.
  4. У callback лежить data.action — та дія, яку хотіли виконати.
  5. «Лишитись» — нічого не dispatch-имо; екран лишається.
  6. «Вийти» — navigation.dispatch(data.action)дозволяємо відкладений leave.

Після успішного збереження важливо скинути dirty (окремий прапорець saved або reset форми), інакше back() після save знову покаже діалог.

dirty
boolean
Умова блокування. Рахуйте чесно: порожня форма не повинна питати «вийти?». Після save — dirty має стати false до або узгоджено з router.back().
usePreventRemove
хук
Підписується на спробу remove екрана. Працює в контексті React Navigation; екран має бути змонтований у навігаторі (modal / stack screen).
data.action
NavigationAction
«Що саме хотіли зробити» (goBack тощо). Без dispatch(action) після «Вийти» екран не закриється — ви лише показали Alert.
navigation
useNavigation()
Потрібен для dispatch. Імпорт з expo-router / React Navigation — за docs вашої версії; у міні-проєкті показано робочий варіант.
Alert
діалог
Системне підтвердження. На різних платформах виглядає по-своєму, але патерн «cancel / destructive» зрозумілий користувачу.

У create-trip Nomad guard у цій статті не додаємо (форма вже велика; Pro-завдання — перенести патерн). У міні-проєкті «Нотатки» — повна реалізація.


Динамічний сегмент [id] — рівно стільки, скільки треба для деталей

Файл:

app/(tabs)/trips/[id].tsx

Маршрути:

  • /trips/1
  • /trips/local-1710000000 (локально створена поїздка)

Читання параметра:

import { useLocalSearchParams } from 'expo-router';

const { id } = useLocalSearchParams<{ id: string }>();
// інколи приходить string[] — нормалізуємо:
const tripId = Array.isArray(id) ? id[0] : id;

Далі:

const trip = tripId ? getTrip(tripId) : undefined;
if (!trip) {
  // екран «не знайдено» + кнопка назад
}

Чому параметр може бути масивом — нюанси парсингу URL/Router; захист Array.isArray — добра звичка.

Чого ми свідомо не робимо в цій статті: query string, universal links, cold start з пуша. Це стаття 13. Тут [id] — лише ключ, щоб відкрити конкретну поїздку з уже відомого списку в пам’яті.


Зв’язок ідей на одному прикладі (Nomad)

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

1. Вкладка «Поїздки»

Працює Tabs. Контент — trips/index у nested Stack. Header списку свій; tab bar видимий.

2. Тап по картці

router.push('/trips/' + id) — push у вкладеному Stack. Анімація «вперед»; tab bar лишається.

3. Екран деталей

Той самий nested Stack. Header системний («назад» → список). Можна відкрити modal «Нова поїздка» — це вже root Stack.

4. Modal create-trip

presentation: 'modal' на root. Tabs під модалкою. Submit → addTrip у Context → router.back() → знову деталі або список (звідки відкрили).

5. Вкладка «Місця» → тап

push на /trips/{tripId} — знову nested detail. Користувач потрапляє в гілку «Поїздки» на екрані деталей.

Якщо на кроці 2 помилково зробити detail root-екраном без nested layout, на кроці 2 tab bar зникне — і ви отримаєте інший продукт, навіть «з тим самим» React-компонентом картки.


Антипатерни (і чому вони болять)

1. Деталі винесено на root без свідомого рішення про tab bar

Симптом: «чомусь пропали вкладки». Насправді файл лежить не в trips/ Stack, а на root. Або навпаки — хотіли full screen, а вклали в tabs. Спочатку UX, потім шлях файлу.

2. Modal як п’ята вкладка

«Нова поїздка» в tab bar завжди висить і скидає стан. Modal-задача — тимчасова; їй місце в Stack options, не в Tabs.

3. Закрити modal через push('/')

Псує історію: back повертає на форму. Після успіху — back() або replace осмислено.

4. Плутати back nested і back root

З деталей чекаєте «закрити застосунок», а отримуєте список — або навпаки. Малюйте дерево навігаторів, перш ніж звинувачувати Router.

5. Dirty-форма без guard

Тиха втрата тексту = втрата довіри. Навіть простий Alert рятує.

6. «Auth = сховали кнопку Увійти»

Без розділення маршрутів / redirect користувач з deep link може опинитись «всередині» без сесії. Групи й guard маршрутів — про послідовність екранів, плюс потім справжня сесія.

7. Товста бізнес-логіка в кожному layout

Layout має з’єднувати навігатори й options, а не тягнути fetch на 200 рядків. Дані — у features / providers (як TripsProvider).


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

Мета

Окремий застосунок поза 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