React Native

Deep linking і параметри маршрутів

Параметри в Expo Router — динамічні сегменти, query, useLocalSearchParams; scheme, universal links і cold start

Deep linking і параметри маршрутів

Параметри маршруту: навіщо вони взагалі

Зв’язок із тим, що вже є

У статтях 11–12 ми вже відкривали конкретну поїздку:

router.push(`/trips/${trip.id}`);

і на екрані деталей читали:

const { id } = useLocalSearchParams<{ id: string }>();

Тобто механізм params у проєкті вже живе. Але він з’явився «по дорозі» — рівно стільки, щоб деталі працювали. Тут ми розкладаємо його з нуля: що саме летить у маршруті, які бувають види параметрів, як їх передавати охайно і що ламається, якщо id немає або прийшов масивом.

Deep linking (відкриття застосунку ззовні за посиланням) — окрема надбудова над тими самими шляхами. Спочатку має сенс міцно зрозуміти params всередині застосунку; потім scheme і universal links стануть природним продовженням, а не магією.

Життєва сцена

Користувач у стрічці бачить двадцять карток. Усі вони ведуть на «той самий» тип екрана — деталі поїздки, — але з різним змістом. Питання до навігації: як сказати системі «відкрий шаблон деталей для поїздки 3», а не «відкрий один фіксований файл trip-carpathians.tsx»?

Відповідь у file-based router: один файл з динамічним сегментом у імені ([id].tsx) і значення цього сегмента в шляху (/trips/3).

На вебі ви вже бачили те саме:

  • /products/42
  • /users/anna/settings
  • /search?q=київ&page=2

На телефоні з Expo Router ідея близька: шлях описує який екран і з якими даними його налаштувати. Відмінність середовища — звідки може прийти цей шлях (тап у застосунку, пуш, QR, Safari), і як ОС віддає його вашому JS. Спочатку розберемо що всередині шляху; відкриття ззовні (scheme, universal links) — логічне продовження тих самих адрес.

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

  1. Пояснити різницю між динамічним сегментом і query-параметром.
  2. Зв’язати ім’я файлу [id].tsx зі шляхом /trips/3.
  3. Передати params через Link, router.push (рядок і об’єкт).
  4. Прочитати params через useLocalSearchParams і безпечно нормалізувати string | string[].
  5. Обробити «немає id» / «поїздки немає в даних» без падіння застосунку.
  6. Коротко відрізнити local і global search params.
  7. Відкрити маршрут ззовні через scheme / links і врахувати cold start.
Головна думка. Параметри — це дані, вкладені в адресу екрана. Динамічний сегмент каже: «який ресурс». Query каже: «як показати / з якими опціями». Читання йде через хуки Expo Router; значення завжди варто вважати рядками ззовні (навіть якщо ви «передавали число»).

Два види параметрів

1. Динамічний сегмент шляху (path / route param)

Динамічний сегмент — частина шляху, яка змінюється і обирає ресурс.

Приклади:

ШляхФайл (схема)Параметр
/trips/3…/trips/[id].tsxid = "3"
/trips/local-1710той самий файлid = "local-1710"
/city/lviv (міні-проєкт ст. 11)city/[id].tsxid = "lviv"

У імені файлу сегмент пишуть у квадратних дужках: [id], [slug], [userId]. Ім’я в дужках стає ключем у об’єкті params.

Це не «папка на всі id». Це один React-екран-шаблон, який Router відкриває знову і знову з різним id.

2. Query-параметри (search / query params)

Query — те, що йде після ?, у вигляді ключ=значення, кілька пар через &:

/trips/3?tab=places
/trips/3?tab=places&highlight=p1

Вони не створюють новий файл маршруту. Той самий [id].tsx може прочитати tab і, наприклад, одразу розгорнути секцію «Місця».

На вебі query звичні для фільтрів і пагінації. У мобільному Router їх теж використовують, але рідше зловживають довгими рядками: зручніше тримати складний UI-стан у React state / глобальному store, а в params — короткі підказки (вкладка, id, прапорець).

Порівняння

Динамічний сегментQuery
Де в URLвсередині path: /trips/3після ?: ?tab=places
Файлпотрібен [id].tsxфайл той самий
Типовий сенсідентичність ресурсуопції показу
Обов’язковістьчасто обов’язковий для маршрутучасто опційний
Прикладяка поїздкаякий таб на екрані деталей

Обидва види потрапляють у search params хуків Expo Router (назви історичні: «search» з вебу). Тобто useLocalSearchParams() може повернути і id, і tab разом.


Файл [id].tsx і шлях

У Nomad уже є:

app/(tabs)/trips/
  _layout.tsx
  index.tsx      → /trips   (або /trips/ залежно від нормалізації)
  [id].tsx       → /trips/:id

Група (tabs) не з’являється в URL. Сегмент tripsз’являється, бо це звичайна тека, не в дужках.

Отже:

  • список: /trips
  • деталі поїздки з id 3: /trips/3

Ім’я параметра id взято з імені файлу [id]. Якби файл називався [tripId].tsx, у params був би ключ tripId, а шлях виглядав би так само структурно: /trips/3, але читали б const { tripId } = useLocalSearchParams().

Узгоджуйте ім’я в дужках, ключ у TypeScript і те, як говорите в команді («id поїздки»). Плутанина id / tripId / tripID — джерело дрібних багів.

Кілька динамічних сегментів

Можна глибше (на майбутнє):

app/trips/[tripId]/places/[placeId].tsx
→ /trips/3/places/p1

У частині 1 достатньо одного сегмента — як у Nomad. Головне зрозуміти правило: кожна [назва] у path ↔ ключ у params.


Як передати параметри

Спосіб A. Рядок у href / push (найнаочніший)

router.push(`/trips/${trip.id}`);
<Link href={`/trips/${trip.id}`}>Відкрити</Link>

Перевага: видно весь шлях. Недолік: легко помилитись у слешах; для typed routes інколи зручніший об’єкт (нижче).

Спосіб B. Об’єкт { pathname, params }

router.push({
  pathname: '/trips/[id]',
  params: { id: trip.id },
});
<Link
  href={{
    pathname: '/trips/[id]',
    params: { id: trip.id },
  }}
>
  Відкрити
</Link>

Тут pathnameшаблон маршруту (як у файловій системі), а params заповнює динамічні шматки і може додати query.

Передача query:

router.push({
  pathname: '/trips/[id]',
  params: {
    id: trip.id,
    tab: 'places', // стане query, бо не є ім’ям сегмента [id] у path… 
  },
});

У Expo Router параметри, які не відповідають динамічному сегменту в path, зазвичай потрапляють у query-частину. Тобто tab не «замінить» id, а додасться як ?tab=places (поведінку завжди перевіряйте на своїй версії Router — ідея саме така: path params vs зайві keys).

Спосіб C. Лише query на статичний шлях

<Link href="/trips?filter=private">Приватні</Link>

Тут немає [id]; екран списку може прочитати filter і відфільтрувати data.

Що передавати: рядок, не «надію на об’єкт»

Params у URL — рядки (або те, що стане рядком). Не варто розраховувати, що через params «пролізе» складний об’єкт Trip. Правильний патерн Nomad:

  1. У маршрут кладете id.
  2. На екрані деталей дістаєте повний Trip з TripsProvider / API за цим id.

Так deep link «відкрий поїздку 3» теж матиме сенс: ззовні прийде лише id, а дані підвантажаться з джерела правди.

Антипатерн. Серіалізувати весь об’єкт у query (?data=...JSON...) — крихко, довго, погано для логів і лімітів URL. Id + сховище/API надійніше.

Як прочитати параметри: useLocalSearchParams

Базовий виклик

На екрані app/(tabs)/trips/[id].tsx:

import { useLocalSearchParams } from 'expo-router';

export default function TripDetailsScreen() {
  const { id } = useLocalSearchParams<{ id: string }>();
  // …
}

Хук повертає об’єкт «ключі → значення з поточного маршруту» (і path, і query — у змішаному вигляді search params).

Дженерик <{ id: string }> допомагає TypeScript; це не runtime-валідація. Якщо id немає, у рантаймі буде undefined.

Чому string | string[] і нормалізація

На практиці значення інколи приходить масивом рядків (особливо з query або крайових випадків парсингу). Тому в Nomad і в міні-проєктах курсу вже є звичка:

const { id } = useLocalSearchParams<{ id: string | string[] }>();
const tripId = Array.isArray(id) ? id[0] : id;

Далі працюєте з tripId: string | undefined.

Це не «перестраховка для галочки» — це захист від рідкісного, але неприємного id.join is not a function / порівнянь масиву з рядком.

Немає id або немає сутності в даних

Два різні провали:

  1. Немає параметра — шлях зібраний криво, опечатка в href.
  2. Параметр є, але getTrip(id) повернув undefined — поїздку видалили, mock скинули pull-to-refresh, id з пуша застарів.

UI в обох випадках має не падати:

if (!tripId || !trip) {
  return (
    <View>
      <Text>Поїздку не знайдено</Text>
      <Button title="Назад" onPress={() => router.back()} />
    </View>
  );
}

У Nomad це вже зроблено на екрані деталей — для deep link ззовні той самий UI «не знайдено» лишається обов’язковим (id з посилання може бути застарілим або хибним).

Local vs Global (коротко; деталі — за потреби пізніше)

Expo Router дає два схожі хуки:

ХукІдея
useLocalSearchParamsParams цього екрана / локального стану маршруту. Типовий вибір для [id].tsx.
useGlobalSearchParamsParams, ближчі до «глобального» URL; оновлюються інакше при навігації по стеку (сусідні екрани в історії).

Для екрана деталей поїздки починайте з local. Global знадобиться, коли свідомо підписуєтесь на зміну URL «вище» по дереву або дебажите розбіжності — не як перший інструмент.

Якщо дані «не ті» після швидких push/pop — перевірте, чи не читаєте global там, де потрібен local (і навпаки). У більшості навчальних екранів local достатньо.

Покроково те, що вже має відбуватись у Nomad при тапі по картці.

1. Користувач на списку

Маршрут на кшталт /trips. Дані — масив trips із Context.

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

Код: router.push(\/trips/${trip.id}`)(або Link з тим самим href). У шлях потрапляє **конкретний** id, наприклад1`.

3. Router обирає файл

Зіставляє path із app/(tabs)/trips/[id].tsx. Сегмент 1 → param id.

4. Монтується екран деталей

useLocalSearchParamsid. Нормалізація → tripId.
getTrip(tripId) → об’єкт поїздки або undefined.

5. Рендер

Є trip — title, опис, місця. Немає — «не знайдено» + back.

6. Back

router.back() у вкладеному Stack → знову список. Param id для списку не потрібен.

Deep link «ззовні» пізніше вклиниться між кроками 0 і 3: ОС відкриє застосунок уже з path /trips/1, і кроки 3–5 виконаються без тапу по картці. Саме тому id у path такий важливий: зовнішнє посилання не може «передати» React-об’єкт, лише адресу.


Типові помилки з params

  1. Опечатка в сегментіpush('/trip/3') при файлі trips/[id].tsx → route not found / wrong screen.
  2. Забули нормалізувати масив — рідкісні креші при порівнянні id.
  3. Чекають об’єкт у params — приходить рядок; об’єкт треба дістати зі store.
  4. Немає UI на missing — білий екран або crash при .title від undefined.
  5. Плутають query і path — фільтр у динамічному сегменті («файл на кожен filter») замість ?filter=.
  6. Жорстко прошиті id в Link — у демо ок; у списку завжди з item.id.

Коротка практика

  1. У Nomad (локально) тимчасово залогуйте useLocalSearchParams() на екрані деталей і відкрийте дві різні поїздки — порівняйте об’єкт params.
  2. Додайте в push query ?from=list і виведіть from під заголовком деталей (потім приберіть).
  3. Відкрийте неіснуючий id (/trips/no-such) через тимчасову кнопку — переконайтесь, що бачите «не знайдено», а не crash.

Deep linking: відкриття застосунку ззовні

Досі ми розглядали шлях всередині вже відкритого застосунку: тап → router.push → params. Deep linking (глибоке посилання) — ситуація, коли адреса приходить ззовні: з браузера, нотаток, QR-коду, пуш-сповіщення, іншого застосунку. ОС дивиться на URL, знаходить ваш застосунок (якщо він зареєстрований на цей тип посилань) і передає URL у нього. Далі Expo Router зіставляє path із файлами в app/ — і ви опиняєтесь на тому ж екрані деталей, що після тапу по картці.

Саме тому params у path були першими: зовнішнє посилання не може передати React-об’єкт Trip. Воно передає лише рядок на кшталт nomad://trips/3. Усе інше — ваш код: прочитати id, знайти дані в store / API, намалювати UI або «не знайдено».

Головна думка. Deep link — це не «інша навігація». Це той самий file-based маршрут, лише вхідна точка інша: не onPress у списку, а ОС із URL. Якщо всередині застосунку /trips/3 уже відкриває деталі, deep link із тим самим path має зробити те саме — за умови, що scheme/асоціація налаштовані.

Custom URL scheme

Що це таке

Custom URL scheme (власна схема URL) — префікс протоколу, який ваша програма реєструє в системі, наприклад nomad:// або myapp://.

Порівняння з вебом:

ВебCustom scheme
Прикладhttps://kostyl.dev/…nomad://trips/3
Хто «володіє»DNS + HTTPSпакет, встановлений на телефоні
Де працюєбудь-який браузерлише якщо встановлено застосунок із цією схемою

Коли користувач (або інший застосунок) відкриває nomad://trips/3:

  1. ОС шукає, хто заявив scheme nomad.
  2. Запускає або «будить» цей застосунок.
  3. Передає повний URL у runtime.
  4. Expo Router / linking шар витягує path (trips/3 → маршрут /trips/3) і відкриває відповідний екран.

Де задається в Expo

У app.json (або app.config.*) поле scheme:

{
  "expo": {
    "scheme": "nomad"
  }
}

У репозиторії Nomad це вже є: "scheme": "nomad". Тобто цільові посилання виглядатимуть як:

nomad://trips/1
nomad://trips/local-1710000000
nomad://create-trip

Після зміни scheme у вже зібраному нативному бінарнику інколи потрібен новий dev build / перезбірка (Expo Go має власні правила — див. нижче). Для чистого JS-оновлення в Expo Go scheme проєкту підхоплюється в dev-сесії, але поведінка відрізняється від production-збірки з «вашим» іменем у магазині.

scheme
string \| string[] у app.json
Ідентифікатор протоколу без://. Лише латиниця, цифри, дефіс — у стилі nomad, my-app. Не плутати зі slug ("slug": "nomad" — id проєкту Expo) і name (відображувана назва).
кілька scheme
масив
Можна передати масив схем, якщо потрібні аліаси (рідше на старті курсу). Для Nomad достатньо однієї.
колізії
ризик
Scheme глобальний на пристрої. Якщо два застосунки заявлять myapp://, поведінка ОС неоднозначна. Для навчального й продакшен-імені обирайте унікальніший префікс (бренд, суфікс).

Анатомія URL зі scheme

Розберемо рядок:

nomad://trips/3?tab=places
ШматокЗначення
nomadscheme (хто відкриває)
trips/3path → маршрут Router (часто нормалізують до /trips/3)
tab=placesquery → params на екрані

Іноді зустрічається host:

nomad://expo-development-client/?url=…

У production custom scheme для ваших екранів зазвичай без зайвого host або з домовленістю, яку очікує Router. У розробці Expo підставляє складніші URL (Expo Go / dev client) — тому для генерації посилань краще не хардкодити nomad://… вручну скрізь, а знати про Linking.createURL (нижче).


Пакет expo-linking

expo-linking — модуль Expo для роботи з URL: зібрати посилання на свій застосунок, розпарсити URL, відкрити зовнішнє посилання, підписатись на вхідні URL.

У Nomad пакет уже в залежностях (expo-linking у package.json) — його тягне екосистема Router.

Linking.createURL(path, options?)

Будує коректний URL для поточного середовища:

  • у production-збірці з "scheme": "nomad" — щось на кшталт nomad:///trips/3 (точний вигляд залежить від платформи/версії);
  • у Expo Go — URL виду exp://… із шляхом, бо Go відкриває проєкти інакше.
import * as Linking from 'expo-linking';

const url = Linking.createURL('/trips/3');
// зручно залогувати або показати в UI «поділитись»
console.log(url);

З query:

const url = Linking.createURL('/trips/3', {
  queryParams: { tab: 'places' },
});
Не хардкодтеnomad://trips/3 у всьому коді «для шерингу», якщо той самий код крутиться в Expo Go і в standalone. createURL підлаштовує base під середовище. Для документації й тестів на пристрої з установленим білдом nomad://… — нормально.

Linking.parse(url)

Розбирає рядок URL на складові (scheme, path, queryParams тощо) — корисно в логах і дебазі:

import * as Linking from 'expo-linking';

const parsed = Linking.parse('nomad://trips/3?tab=places');
// path / hostname / queryParams — дивіться об’єкт у консолі

Linking.openURL(url)

Відкриває URL системно: браузер, інший застосунок, інколи знову ваш (якщо scheme ваш).

await Linking.openURL('https://kostyl.dev');
// або
await Linking.openURL(Linking.createURL('/trips/1'));

Перед відкриттям інколи перевіряють Linking.canOpenURL (на iOS для чужих scheme можуть знадобитись записи в Info.plist / config plugins — для відкриття свого scheme зсередини зазвичай простіше).

Linking.useURL() / події

Хук useURL() повертає URL, з яким застосунок відкрили (або null). Корисно для діагностики: «з яким рядком мене зараз відкрили?».

Expo Router сам підписується на вхідні URL і навигує на відповідний маршрут, якщо path збігається з app/. Вам рідко треба вручну парсити кожен deep link і викликати router.push — але розуміти шар linking потрібно, коли щось «не відкривається» або path приходить у несподіваному вигляді.

createURL
(path, options?) => string
Зібрати deep link на цей застосунок у поточному середовищі (Go vs standalone). Path краще з провідним / для маршрутів Router: '/trips/3'.
parse
(url) => ParsedURL
Розкласти URL на path / query / scheme для логів і власної логіки.
openURL
(url) => Promise
Віддати URL системі (браузер, maps, інший app, інколи свій).
useURL
hook → string \| null
Поточний «вхідний» URL сесії відкриття. Для UI-діагностики; навігацією зазвичай керує Router.
canOpenURL
(url) => Promise<boolean>
Чи є на пристрої хтось, хто відкриє цей URL. Обмеження платформ (особливо iOS) читайте в docs, коли підете в інтеграції з чужими scheme.

Як перевірити scheme на практиці

1. Переконайтесь, що scheme у конфігу

Nomad: "scheme": "nomad" у app.json. Після зміни — перезапуск / новий білд за потреби.

2. Expo Go vs dev client / standalone

СередовищеЩо очікувати
Expo GoDeep link часто йде через exp:// і UI Go; custom scheme проєкту працює обмежено / інакше, ніж у магазині. Для навчання params всередині app — Go достатньо; для «як у проді nomad://» надійніше development build або preview build.
Dev client / preview / storeCustom scheme з app.json реєструється в нативному проєкті; nomad://trips/3 має відкривати застосунок.

Курс не вимагає одразу EAS build для кожної вправи, але варто знати: «у Go не відкрилось nomad://» ≠ «Router зламаний».

3. Команди й системні способи тесту

iOS Simulator (якщо білд установлений):

xcrun simctl openurl booted "nomad://trips/1"

Android (емулятор / девайс з adb, пакет установлений):

adb shell am start -a android.intent.action.VIEW -d "nomad://trips/1"

У застосунку: кнопка «Відкрити тестове посилання»:

import * as Linking from 'expo-linking';

await Linking.openURL(Linking.createURL('/trips/1'));

Нотатки / Safari: вставити nomad://trips/1 (на реальному пристрої з установленим білдом). Поведінка Safari з custom scheme різниться; інколи зручніше Notes або QR.

4. Що має статись у Nomad

Якщо Router і вкладений стек налаштовані (ст. 11–12):

  1. Відкривається застосунок (або виходить на передній план).
  2. Навігація веде на екран trips/[id] з id=1.
  3. Якщо поїздка є в TripsProvider — деталі; якщо ні — «не знайдено».

Перевіряйте обидва стани: валідний id з mock і свідомо невалідний (nomad://trips/nope).


Cold start і warm start (на рівні ідеї)

Коли deep link відкриває застосунок, важливий стан процесу.

Cold start (холодний старт) — застосунок не крутився в пам’яті (або був убитий системою). ОС запускає процес з нуля: JS-бандл, providers, Router, потім (або разом) обробка URL. Час до першого кадру довший; порядок ініціалізації критичний (провайдери мають існувати, перш ніж екран деталей читає getTrip).

Warm start (теплий) — застосунок уже був у фоні. ОС лише передає новий URL; Router навигує на маршрут. Стан у пам’яті (наприклад, уже завантажені trips) може зберегтись.

cold start
сценарій
Повне підняття JS. Deep link на /trips/3 має спрацювати після того, як root layout змонтував TripsProvider. Якщо дані лише «випадково» були в state з попередньої сесії — на cold start їх не буде: потрібні mock seed, persist (MMKV — пізніше) або fetch.
warm start
сценарій
Процес живий. Новий URL змінює маршрут. Можливі нюанси: уже відкрита modal, dirty-форма, інша вкладка — deep link «перебиває» UI; продуктово вирішуйте, чи завжди replace на ціль.
initialRouteName / unstable_settings
якір
Як у статті про modal: якір (tabs) допомагає мати базу історії, коли вхід одразу на глибокий екран.

Детальний порядок «що першим: splash, auth, url» у великих застосунках розростається. На рівні курсу достатньо: cold start = даних у RAM може не бути; path у URL — єдине, що точно прийшло ззовні.


Зв’язок scheme і params (зшивка)

Повний шлях даних для nomad://trips/3:

1. Зовнішній світ

Користувач відкриває посилання (Notes, QR, adb, інший app).

2. ОС

Бачить scheme nomad → знаходить пакет Nomad → стартує / resume.

3. Linking + Expo Router

URL → path /trips/3 (нормалізація) → файл app/(tabs)/trips/[id].tsx.

4. Params

useLocalSearchParamsidtripId = "3".

5. Дані

getTrip("3") з Context / API. Не з URL (у URL лише id).

6. UI

Деталі або «не знайдено». Back / tabs — як у звичайній внутрішній навігації.

Якщо крок 5 падає на cold start (порожній store) — це вже архітектура даних, не «Router не вміє deep link». Mock у TripsProvider при старті якраз рятує демо; у проді з’являться persist і мережа.


Типові помилки з scheme і linking

  1. Забули scheme у app.json — custom URL «нічий».
  2. Тестують nomad:// лише в Expo Go і роблять висновок, що все зламано.
  3. Хардкод nomad:// у коді, який крутиться в Go — краще createURL для шерингу з того ж білда.
  4. Path без узгодження з app/nomad://trip/3 при файлі trips/[id].
  5. Очікують об’єкт Trip у URL — ззовні лише рядки.
  6. Не перевіряють missing entity на cold start.
  7. Колізія scheme з іншим навчальним додатком на тому ж симуляторі.

Практика

  1. Виведіть на екран «Ще» у Nomad результат Linking.createURL('/trips/1') (тимчасовий AppText) — порівняйте рядок у Go і, якщо є, у standalone.
  2. З симулятора / adb надішліть nomad://trips/1 на білд, де scheme зареєстровано.
  3. Надішліть nomad://trips/does-not-exist — має бути спокійний empty/error UI.
  4. Відкрийте deep link, коли застосунок уже на екрані форми create-trip (warm) — подивіться, чи прийнятна поведінка.

Custom scheme (nomad://…) зручний для розробки й внутрішніх сценаріїв. Для користувача в месенджері чи пошті частіше хочуть звичайне посилання:

https://app.example.com/trips/3

Якщо застосунок установлений і домен прив’язаний до нього, система може відкрити застосунок, а не (або не лише) браузер. Якщо застосунку немає — відкривається сайт (fallback). Це і є ідея Universal Links (iOS) та App Links (Android).

Навіщо ускладнення

Custom scheme nomad://https://… (universal / app links)
Працює без доменуПотрібен ваш домен і файли на ньому
Якщо app немає — часто «нічого» / помилкаЯкщо app немає — веб-сторінка
У чатах інколи виглядає «підозріло»Звичайне https-посилання
Швидко для devПотрібна верифікація ОС (AASA / assetlinks)
Колізії scheme між appsПрив’язка до bundle id + сертифікатів

У проді обидва підходи часто співіснують: https для людей, scheme для внутрішніх інструментів і legacy.

Як це влаштовано (ідея, не чекліст магазину)

Система не вірить застосунку на слово «я відкриваю example.com». Потрібна двостороння асоціація:

  1. На сайті лежить файл, який каже: «ці path відкриває app з таким id».
  2. У застосунку (нативна конфігурація) заявлено: «я обслуговую цей домен».
  • У app.json / config: ios.associatedDomains, наприклад applinks:app.example.com (без https:// у цьому рядку — поширена помилка).
  • На сервері: файл Apple App Site Association (AASA), зазвичай
    https://app.example.com/.well-known/apple-app-site-association
    (без зайвого .json у проді Apple, content-type як у docs).
  • Усередині — appID = TeamID + bundle identifier і список paths (/trips/* тощо).
  • Збірка з entitlements Associated Domains (EAS Build підхоплює з config).
  • У config: android.intentFilters з https, host, pathPrefix, часто autoVerify: true.
  • На сервері:
    https://app.example.com/.well-known/assetlinks.json
    з package_name і SHA-256 відбитками підпису (debug і release — різні!).
  • Система верифікує файл; після успіху https-лінки можуть відкривати app без діалогу «чим відкрити».

3. Expo / EAS

Конфіг у app.json / app.config.js — джерело для prebuild. Самі AASA / assetlinks не з’являються «самі» на домені: їх викладає бекенд / static hosting / CDN. Для курсу достатньо розуміти ланцюг; повний setup з реальним доменом — окремий інфраструктурний крок продукту.

Приклад фрагментів config (орієнтир)

iOS associated domain:

{
  "expo": {
    "ios": {
      "bundleIdentifier": "com.example.nomad",
      "associatedDomains": ["applinks:app.example.com"]
    }
  }
}

Android intent filter (схема):

{
  "expo": {
    "android": {
      "package": "com.example.nomad",
      "intentFilters": [
        {
          "action": "VIEW",
          "autoVerify": true,
          "data": [
            {
              "scheme": "https",
              "host": "app.example.com",
              "pathPrefix": "/trips"
            }
          ],
          "category": ["BROWSABLE", "DEFAULT"]
        }
      ]
    }
  }
}

Це не копіювати сліпо в Nomad без реального домену — інакше білд лише «заявить» права, які ОС не підтвердить.

Що спільного з params

Після того, як ОС віддала застосунку URL:

https://app.example.com/trips/3

Router знову бачить path /trips/3, [id]3, useLocalSearchParams — як після nomad://trips/3 або внутрішнього push. Екран і params ті самі; відрізняється лише транспорт доставки URL.

Веб-fallback. Якщо публікуєте https-лінки, хтось без app потрапить на сайт. Сторінка /trips/3 на вебі має існувати або редіректити розумно — інакше «мертве» посилання. Це продуктове рішення (маркетинг + app), не лише RN.

Коли в курсі що використовувати

ЗадачаРекомендація курсу
Навчання params і Routerвнутрішній push + scheme у dev
Демо deep link на симуляторіnomad://… + dev/preview build
Прод «поділитись у Telegram»https + universal/app links (коли є домен)
Expo Goне критерій «прод deep link»

Міні-проєкт: «Купон за лінком» (від А до Я)

Мета

Окремий застосунок поза Nomad. Відкриваєте couponapp://coupon/SUMMER25 (або createURL) → екран купона з кодом. Повний код нижче.

Що має вийти

  1. Головний екран зі списком купонів (тап → деталі).
  2. Екран /coupon/[code] — великі літери коду, опис, кнопка «Скопіювати» (опційно Alert).
  3. Кнопка «Відкрити deep link» через Linking.openURL(createURL(...)).
  4. Невідомий код → спокійний «Купон не знайдено».
  5. У app.json"scheme": "couponapp".

Структура

coupon-app/
  app/
    _layout.tsx
    index.tsx
    coupon/
      [code].tsx
  src/
    data/
      coupons.ts
  app.json
  package.json

Крок 1. Проєкт

1. Скарфолд

npx create-expo-app@latest coupon-app -t blank-typescript
cd coupon-app

2. Router і залежності

npx expo install expo-router expo-linking expo-constants expo-status-bar react-native-safe-area-context react-native-screens

У package.json встановіть:

"main": "expo-router/entry"

3. Scheme

У app.json всередині "expo":

"scheme": "couponapp",
"plugins": ["expo-router"]

4. Запуск після файлів

npx expo start

Крок 2. Дані

Створіть src/data/coupons.ts:

Крок 3. app/_layout.tsx

Крок 4. Головна — app/index.tsx

Крок 5. Екран купона — app/coupon/[code].tsx

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

  1. Список купонів → тап → екран коду.
  2. «Відкрити deep link» → той самий екран через linking.
  3. Неіснуючий код → «не знайдено».
  4. На білді з scheme:
    xcrun simctl openurl booted "couponapp://coupon/SUMMER25"
    або adb з couponapp://coupon/SUMMER25.

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

  • scheme: couponapp
  • Повні файли з collapsible
  • Path param code + нормалізація регістру
  • createURL + openURL
  • Missing coupon UI

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

Поділитись посиланням на конкретну поїздку (у навчанні — показати URL і відкрити його). Зовнішній світ передає лише id; екран деталей і getTrip уже вміють решту.

Нитка

Уже є: scheme: "nomad", typedRoutes: true, /trips/[id], useLocalSearchParams, nested stack, modal create-trip.

Додаємо:

  • на екрані деталей — рядок Linking.createURL('/trips/' + id) і кнопка «Відкрити це посилання»;
  • на вкладці «Ще» — підказка scheme + приклад createURL для /trips/1.

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

Як перевірити

  1. Відкрийте поїздку id 1 зі списку — блок Deep link показує URL.
  2. «Відкрити це посилання» — Router знову веде на деталі.
  3. На preview/dev build:
    xcrun simctl openurl booted "nomad://trips/1"
    або adb з тим самим URI.
  4. nomad://trips/nope → «Поїздку не знайдено».
  5. Вкладка «Ще» — текст scheme і createURL.

Коміт

cd /path/to/nomad
git add -A
git commit -m "$(cat <<'EOF'
feat: typed routes and deep link to trip

Material: content/15.react-native/13.deep-linking-and-params.md
EOF
)"
git push

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

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

  1. Своїми словами: path param vs query.
  2. Чим nomad://trips/3 відрізняється від https://…/trips/3 для користувача без app.
  3. Навіщо Array.isArray(id) ? id[0] : id.

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

  1. Міні-проєкт «Купон» повністю.
  2. У Nomad додати query ?from=share у createURL і показати from на деталях.
  3. Порівняти рядок createURL у Expo Go і (якщо є) у standalone.

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

  1. Чернетка associatedDomains + AASA paths для /trips/* (без обов’язкового деплою).
  2. Описати cold start: що буде з trips, якщо прибрати mock seed (гіпотетично).
  3. Список ризиків: відкритий id у URL, enumeration, застарілі посилання.

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


Що далі

Модуль навігації закрито: params, scheme, уявлення про universal links, cold/warm start. Далі — мережа й життєвий цикл застосунку: fetch, стани помилок, AppState, офлайн-банер — щоб deep link на поїздку міг підвантажити дані не лише з mock.

Expo linking · iOS Universal Links · Android App Links · URL parameters · Nomad

Copyright © 2026