Как делать11 октября 2026 г.7 мин чтения
React Native + ExpoШаг 4 из 9

Навигация в Expo Router: стеки, вкладки, параметры

Коротко

В Expo Router каждый файл в src/app — это экран со своим адресом: notes/[id].tsx обслуживает /notes/1, /notes/2 и так далее, а _layout.tsx решает, как экраны папки связаны — стеком (Stack) или вкладками (Tabs). Переходят через <Link href> или объект router (push, navigate, replace, back), параметр читают хуком useLocalSearchParams. Поскольку у каждого экрана есть адрес, глубокие ссылки вида notesapp://notes/1 работают без отдельной настройки.

Пошаговый урокУровень: СреднийМобильная навигацияПрактика: ~1,5–3 ч

После материала вы сможете

  • описать маршруты файлами в src/app: index, динамический [id], группу (tabs) и _layout
  • настроить корневой Stack с вкладками Tabs и модальным экраном и перейти между ними через Link и router
  • передать параметр в динамический маршрут, прочитать его через useLocalSearchParams и безопасно вернуться назад после глубокой ссылки
  • проверить навигацию без телефона: типизированные маршруты в tsc и тест на renderRouter

Проверено на

Expo SDK 57 (expo 57.0.27)Expo Router 57.0.25React Native 0.86.3React 19.2.3TypeScript 6.0

Сверено с документацией: 11 октября 2026 г.

Содержание
  1. 01Маршруты — это файлы в src/app
  2. 02_layout: стек и вкладки
  3. 03Link и router: переходы между экранами
  4. 04Динамический маршрут и параметры
  5. 05Модальное окно создания записи
  6. 06Глубокие ссылки
  7. 07Типизированные маршруты, тесты и упражнения

Маршруты — это файлы в src/app

Expo Router — файловый маршрутизатор для React Native и веба. Документация формулирует правило просто: все маршруты приложения задаются файлами и папками внутри src/app, у каждого экрана есть URL-путь, совпадающий с расположением файла, а первым открывается index.tsx. Код, который не является экраном, — компоненты, хуки, данные — держат вне src/app, иначе Expo Router попробует сделать из него маршрут.

Практика урока — приложение «Записи»: две вкладки (список и «О приложении»), экран записи с параметром в адресе и модальное окно создания. Проект создан из шаблона по умолчанию, как в предыдущем уроке, примеры сброшены, добавлены иконки.

Terminal
npx create-expo-app@latest notes-app --yes
cd notes-app
npm run reset-project
npx expo install @expo/vector-icons
Шаблон Expo SDK 57 уже содержит Expo Router; иконки ставим через npx expo install

src/app/_layout.tsx

Адрес
—
Роль
Корневой стек

src/app/(tabs)/_layout.tsx

Адрес
—
Роль
Вкладки

src/app/(tabs)/index.tsx

Адрес
/
Роль
Список записей

src/app/(tabs)/about.tsx

Адрес
/about
Роль
О приложении

src/app/notes/[id].tsx

Адрес
/notes/1, /notes/2, …
Роль
Запись

src/app/new.tsx

Адрес
/new
Роль
Модальное окно

src/data/notes.tsx

Адрес
—
Роль
Данные, не маршрут
Файлы проекта и адреса, которые они получают

index.tsx

Что значит
Экран по умолчанию для папки
Пример
(tabs)/index.tsx → /

[id].tsx

Что значит
Динамический сегмент: подходит под любое значение
Пример
notes/[id].tsx → /notes/42

(tabs)/

Что значит
Группа: объединяет экраны, но не попадает в адрес
Пример
(tabs)/about.tsx → /about

_layout.tsx

Что значит
Навигатор папки: как связаны её экраны
Пример
Stack, Tabs

+not-found.tsx

Что значит
Экран для адресов, которым ничего не соответствует
Пример
Expo Router показывает свой, если файла нет
Обозначения в именах файлов

_layout: стек и вкладки

Корневой _layout.tsx заменяет App.tsx: он рендерится раньше всех экранов, поэтому сюда помещают провайдеры, шрифты и тему. Записи храним в React-контексте, чтобы список и модальное окно работали с одними данными. Сохранение между запусками — тема следующего урока, здесь данные живут в памяти.

src/data/notes.tsx
import { createContext, use, useState, type ReactNode } from 'react';

export type Note = {
  id: string;
  title: string;
  body: string;
};

type NotesContextValue = {
  notes: Note[];
  addNote: (title: string, body: string) => Note;
};

const NotesContext = createContext<NotesContextValue | null>(null);

const initialNotes: Note[] = [
  { id: '1', title: 'Call the supplier', body: 'Confirm the delivery date for Friday.' },
  { id: '2', title: 'Team meeting', body: 'Prepare the agenda and the weekly report.' },
];

export function NotesProvider({ children }: { children: ReactNode }) {
  const [notes, setNotes] = useState(initialNotes);

  function addNote(title: string, body: string) {
    const note = { id: String(Date.now()), title, body };
    setNotes((current) => [note, ...current]);
    return note;
  }

  return <NotesContext value={{ notes, addNote }}>{children}</NotesContext>;
}

export function useNotes() {
  const value = use(NotesContext);
  if (!value) {
    throw new Error('useNotes must be used inside NotesProvider');
  }
  return value;
}
Данные и контекст: файл вне src/app, поэтому не становится маршрутом
src/app/_layout.tsx
import { Stack } from 'expo-router';

import { NotesProvider } from '@/data/notes';

export default function RootLayout() {
  return (
    <NotesProvider>
      <Stack>
        <Stack.Screen name="(tabs)" options={{ headerShown: false }} />
        <Stack.Screen name="notes/[id]" options={{ title: 'Note' }} />
        <Stack.Screen name="new" options={{ presentation: 'modal', title: 'New note' }} />
      </Stack>
    </NotesProvider>
  );
}
Корневой стек: группа вкладок без заголовка, экран записи и модальное окно

Stack — стек экранов: новый экран ложится сверху, кнопка или жест «назад» снимает его. Stack.Screen здесь не создаёт маршруты (их создают файлы), а настраивает их: заголовок, видимость шапки, способ показа. Группа (tabs) — один экран корневого стека, внутри которого свой навигатор.

src/app/(tabs)/_layout.tsx
import Ionicons from '@expo/vector-icons/Ionicons';
import { Link, Tabs } from 'expo-router';
import { Pressable, StyleSheet, Text } from 'react-native';

export default function TabLayout() {
  return (
    <Tabs screenOptions={{ tabBarActiveTintColor: '#2563eb' }}>
      <Tabs.Screen
        name="index"
        options={{
          title: 'Notes',
          tabBarIcon: ({ color, size }) => (
            <Ionicons name="document-text-outline" size={size} color={color} />
          ),
          headerRight: () => (
            <Link href="/new" asChild>
              <Pressable accessibilityRole="button" style={styles.headerButton}>
                <Text style={styles.headerButtonText}>New</Text>
              </Pressable>
            </Link>
          ),
        }}
      />
      <Tabs.Screen
        name="about"
        options={{
          title: 'About',
          tabBarIcon: ({ color, size }) => (
            <Ionicons name="information-circle-outline" size={size} color={color} />
          ),
        }}
      />
    </Tabs>
  );
}

const styles = StyleSheet.create({
  headerButton: {
    marginRight: 16,
    paddingHorizontal: 12,
    paddingVertical: 6,
    borderRadius: 8,
    backgroundColor: '#2563eb',
  },
  headerButtonText: {
    color: '#ffffff',
    fontWeight: '600',
  },
});
Вкладки: подписи, иконки и кнопка «New» в шапке списка

Tabs из expo-router — JavaScript-вкладки: одинаково выглядят на всех платформах и гибко настраиваются через screenOptions (для всех вкладок) и options (для одной). Иконка получает color и size от навигатора, поэтому активная вкладка подсвечивается сама. Чтобы спрятать маршрут из панели вкладок, ему задают href: null.

Link и router: переходы между экранами

Декларативный переход — компонент Link. По умолчанию он оборачивает содержимое в Text; с asChild передаёт нажатие дочернему компоненту, например Pressable со своими стилями. Адрес можно задать строкой (/notes/1) или объектом с pathname и params — второй вариант удобнее, когда значение берётся из данных.

src/app/(tabs)/index.tsx
import { Link } from 'expo-router';
import { FlatList, Pressable, StyleSheet, Text } from 'react-native';

import { useNotes } from '@/data/notes';

export default function NotesScreen() {
  const { notes } = useNotes();

  return (
    <FlatList
      data={notes}
      keyExtractor={(note) => note.id}
      contentContainerStyle={styles.list}
      ListEmptyComponent={<Text style={styles.empty}>No notes yet</Text>}
      renderItem={({ item }) => (
        <Link href={{ pathname: '/notes/[id]', params: { id: item.id } }} asChild>
          <Pressable style={styles.row}>
            <Text style={styles.title}>{item.title}</Text>
            <Text style={styles.preview} numberOfLines={1}>
              {item.body}
            </Text>
          </Pressable>
        </Link>
      )}
    />
  );
}

const styles = StyleSheet.create({
  list: {
    padding: 16,
    gap: 12,
  },
  row: {
    padding: 16,
    borderRadius: 12,
    backgroundColor: '#ffffff',
    gap: 4,
  },
  title: {
    fontSize: 17,
    fontWeight: '600',
    color: '#18181b',
  },
  preview: {
    fontSize: 15,
    color: '#71717a',
  },
  empty: {
    textAlign: 'center',
    color: '#71717a',
  },
});
Список записей: каждая строка — Link на экран записи
src/app/(tabs)/about.tsx
import { Link } from 'expo-router';
import { StyleSheet, Text, View } from 'react-native';

export default function AboutScreen() {
  return (
    <View style={styles.container}>
      <Text style={styles.text}>Notes is a demo of Expo Router: tabs, a stack and a modal.</Text>
      <Link href="/notes/1" style={styles.link}>
        Open the first note
      </Link>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    padding: 16,
    gap: 16,
  },
  text: {
    fontSize: 16,
    lineHeight: 24,
    color: '#3f3f46',
  },
  link: {
    fontSize: 16,
    color: '#2563eb',
  },
});
Вкладка «О приложении»: Link со строковым адресом

navigate(href)

Что делает
Открывает экран или возвращается к нему, если он уже в стеке
Когда нужен
Обычный переход — вариант по умолчанию

push(href)

Что делает
Всегда кладёт новый экран, даже если такой уже есть
Когда нужен
Цепочка однотипных экранов: запись → связанная запись

replace(href)

Что делает
Подменяет текущий экран без записи в историю
Когда нужен
После входа в аккаунт, после сохранения формы

back() / canGoBack()

Что делает
Шаг назад / есть ли куда возвращаться
Когда нужен
Своя кнопка «назад»

dismissTo(href)

Что делает
Закрывает экраны до указанного, иначе заменяет текущий
Когда нужен
Закрыть модальное окно и вернуться к списку

setParams(params)

Что делает
Меняет параметры текущего экрана без новой записи в истории
Когда нужен
Поиск и фильтры в адресе
Методы router из useRouter() или импорта router

В веб-сборке Link превращается в настоящую ссылку: в нашей проверке кнопка «New» в браузере отрисовалась как <a href="/new">. Её можно открыть в новой вкладке, а переход идёт без перезагрузки страницы — состояние JavaScript сохраняется.

Динамический маршрут и параметры

Файл notes/[id].tsx обслуживает любые адреса вида /notes/<id>. Значение сегмента возвращает хук useLocalSearchParams — типизируем его дженериком. Параметры адреса всегда строки (у сегментов вида [...rest] — массивы строк), поэтому числа и даты разбирают явно.

src/app/notes/[id].tsx
import { Stack, useLocalSearchParams, useRouter } from 'expo-router';
import { Pressable, StyleSheet, Text, View } from 'react-native';

import { useNotes } from '@/data/notes';

export default function NoteScreen() {
  const { id } = useLocalSearchParams<{ id: string }>();
  const { notes } = useNotes();
  const router = useRouter();
  const note = notes.find((item) => item.id === id);

  function goBack() {
    if (router.canGoBack()) {
      router.back();
    } else {
      router.replace('/');
    }
  }

  return (
    <View style={styles.container}>
      <Stack.Screen options={{ title: note?.title ?? 'Not found' }} />
      <Text style={styles.body}>{note ? note.body : `Note ${id} does not exist.`}</Text>
      <Pressable accessibilityRole="button" onPress={goBack} style={styles.button}>
        <Text style={styles.buttonText}>Back to notes</Text>
      </Pressable>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    padding: 16,
    gap: 24,
  },
  body: {
    fontSize: 17,
    lineHeight: 26,
    color: '#18181b',
  },
  button: {
    alignSelf: 'flex-start',
    paddingHorizontal: 16,
    paddingVertical: 10,
    borderRadius: 10,
    backgroundColor: '#e4e4e7',
  },
  buttonText: {
    fontSize: 16,
    color: '#18181b',
  },
});
Экран записи: параметр из адреса, заголовок из данных, безопасный возврат
  1. Заголовок из данных

    <Stack.Screen options={...} /> внутри экрана меняет его шапку, когда данные уже известны, — так в заголовке оказывается название записи, а не общее «Note».

  2. Неизвестный id

    Запись могли удалить, а ссылка осталась. Экран не падает, а показывает понятное сообщение.

  3. Возврат без истории

    Если экран открыт по глубокой ссылке, предыдущего экрана в стеке нет и router.back() вернуться не сможет. Поэтому сначала проверяем router.canGoBack(), а иначе заменяем экран на список.

  4. Локальные и глобальные параметры

    useLocalSearchParams обновляется, только когда адрес относится к этому экрану, и не перерисовывает экраны, оставшиеся ниже в стеке. useGlobalSearchParams следит за любым адресом — документация предупреждает о лишних перерисовках.

Модальное окно создания записи

Модальный экран — обычный маршрут, которому в стеке задали presentation: 'modal' (это сделано в корневом _layout.tsx). На iOS он выезжает снизу и закрывается свайпом вниз, на Android закрывается кнопкой «назад». В вебе документация требует дать пользователю способ закрыть окно самостоятельно — для этого кнопка «Cancel».

src/app/new.tsx
import { useRouter } from 'expo-router';
import { useState } from 'react';
import { Pressable, StyleSheet, Text, TextInput, View } from 'react-native';

import { useNotes } from '@/data/notes';

export default function NewNoteScreen() {
  const router = useRouter();
  const { addNote } = useNotes();
  const [title, setTitle] = useState('');
  const [body, setBody] = useState('');
  const canSave = title.trim().length > 0;

  function save() {
    addNote(title.trim(), body.trim());
    router.dismissTo('/');
  }

  return (
    <View style={styles.container}>
      <TextInput
        placeholder="Title"
        value={title}
        onChangeText={setTitle}
        style={styles.input}
        autoFocus
      />
      <TextInput
        placeholder="Text"
        value={body}
        onChangeText={setBody}
        style={[styles.input, styles.multiline]}
        multiline
      />
      <Pressable
        accessibilityRole="button"
        accessibilityState={{ disabled: !canSave }}
        disabled={!canSave}
        onPress={save}
        style={[styles.button, !canSave && styles.buttonDisabled]}>
        <Text style={styles.buttonText}>Save</Text>
      </Pressable>
      <Pressable accessibilityRole="button" onPress={() => router.dismissTo('/')}>
        <Text style={styles.cancel}>Cancel</Text>
      </Pressable>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    padding: 16,
    gap: 12,
  },
  input: {
    borderWidth: 1,
    borderColor: '#d4d4d8',
    borderRadius: 10,
    padding: 12,
    fontSize: 16,
  },
  multiline: {
    minHeight: 120,
    textAlignVertical: 'top',
  },
  button: {
    alignItems: 'center',
    padding: 14,
    borderRadius: 10,
    backgroundColor: '#2563eb',
  },
  buttonDisabled: {
    opacity: 0.4,
  },
  buttonText: {
    color: '#ffffff',
    fontSize: 16,
    fontWeight: '600',
  },
  cancel: {
    textAlign: 'center',
    padding: 8,
    color: '#71717a',
  },
});
Модальное окно: форма, сохранение и закрытие через dismissTo

router.dismissTo('/') закрывает экраны, пока не дойдёт до списка. Если окно открыто напрямую по адресу /new и списка в истории нет, метод заменит текущий экран на список — отдельная проверка, как в экране записи, не нужна.

Глубокие ссылки

Глубокая ссылка открывает конкретный экран приложения. Схема задаётся полем scheme в app.json — в нашем проекте шаблон уже записал "scheme": "notesapp". Expo Router включает глубокие ссылки для всех экранов автоматически: notesapp://notes/1 откроет экран записи с id = "1", ничего дополнительно описывать не нужно. После добавления или смены схемы приложение пересобирают.

Terminal
# development build или эмулятор с установленным приложением
npx uri-scheme open notesapp://notes/1 --android
npx uri-scheme open notesapp://notes/1 --ios

# Expo Go: путь внутри приложения идёт после /--/
exp://127.0.0.1:8081/--/notes/1
Команды из документации Expo для проверки ссылки на устройстве или эмуляторе
  • Ссылки со своей схемой не работают, если приложение не установлено. Для этого случая есть универсальные ссылки — обычные адреса https://, которые открывают приложение или сайт. Android называет их App Links, iOS — Universal Links; обе требуют файла подтверждения на вашем домене.
  • Экран, открытый по ссылке, оказывается первым в стеке. Поэтому кнопка «назад» на экране записи проверяет canGoBack().
  • Если приложение публикуется и как сайт со статическим экспортом ("output": "static" в шаблоне), для динамических маршрутов нужна функция generateStaticParams со списком известных значений — иначе страницы /notes/2 в сборке не будет.

Типизированные маршруты, тесты и упражнения

В шаблоне включён флаг experiments.typedRoutes: Expo генерирует типы всех маршрутов, и TypeScript проверяет каждый href. Документация называет функцию бета-версией. Типы появляются при запуске npx expo start, а без запуска сервера — после npx expo customize tsconfig.json. Мы проверили, что ошибки действительно ловятся: опечатка в пути и неверное имя параметра.

src/tmp/typed-check.tsx
<Link href="/note/1">Typo in the path</Link>
<Link href={{ pathname: '/notes/[id]', params: { noteId: '1' } }}>Wrong param</Link>
Фрагмент, специально написанный с ошибками; после проверки удалён
Terminal
$ npx tsc --noEmit
src/tmp/typed-check.tsx(6,13): error TS2322: Type '"/note/1"' is not assignable to type '"/" | RelativePathString | ExternalPathString | "/new" | …
src/tmp/typed-check.tsx(7,46): error TS2322: Type '{ noteId: string; }' is not assignable to type 'UnknownInputParams & { id: string | number; }'.
  Property 'id' is missing in type '{ noteId: string; }' but required in type '{ id: string | number; }'.
Реальный вывод tsc, длинный список допустимых путей сокращён

Навигацию можно проверить без телефона: expo-router/testing-library рендерит приложение из папки маршрутов с заданным начальным адресом. Тест ниже проходит путь «список → запись → назад», открывает запись по глубокой ссылке, создаёт запись через модальное окно и проверяет запасной экран для неизвестного id.

__tests__/navigation-test.tsx
/// <reference types="jest" />
import { router } from 'expo-router';
import { act, fireEvent, renderRouter, screen } from 'expo-router/testing-library';

const appDir = './src/app';

describe('Notes navigation', () => {
  test('list → note details → back', async () => {
    const app = renderRouter(appDir, { initialUrl: '/' });
    await app;
    expect(app.getPathname()).toBe('/');

    await fireEvent.press(screen.getByText('Call the supplier'));
    expect(app.getPathname()).toBe('/notes/1');
    expect(app.getSearchParams()).toEqual({ id: '1' });
    expect(screen.getByText('Confirm the delivery date for Friday.')).toBeTruthy();

    await fireEvent.press(screen.getByRole('button', { name: 'Back to notes' }));
    expect(app.getPathname()).toBe('/');
  });

  test('deep link opens a note without history', async () => {
    const app = renderRouter(appDir, { initialUrl: '/notes/2' });
    await app;
    expect(app.getPathname()).toBe('/notes/2');
    expect(screen.getByText('Prepare the agenda and the weekly report.')).toBeTruthy();

    await fireEvent.press(screen.getByRole('button', { name: 'Back to notes' }));
    expect(app.getPathname()).toBe('/');
  });

  test('modal creates a note and closes', async () => {
    const app = renderRouter(appDir, { initialUrl: '/' });
    await app;
    await act(() => router.push('/new'));
    expect(app.getPathname()).toBe('/new');

    await fireEvent.changeText(screen.getByPlaceholderText('Title'), 'Buy coffee beans');
    await fireEvent.press(screen.getByRole('button', { name: 'Save' }));

    expect(app.getPathname()).toBe('/');
    expect(screen.getByText('Buy coffee beans')).toBeTruthy();
  });

  test('unknown id shows a fallback', async () => {
    await renderRouter(appDir, { initialUrl: '/notes/999' });
    expect(screen.getByText('Note 999 does not exist.')).toBeTruthy();
  });
});
Тест навигации: Jest + jest-expo + React Native Testing Library 14 (установка — как в предыдущем уроке)
Terminal
$ npm test
PASS __tests__/navigation-test.tsx
  Notes navigation
    √ list → note details → back (352 ms)
    √ deep link opens a note without history (29 ms)
    √ modal creates a note and closes (70 ms)
    √ unknown id shows a fallback (10 ms)

Tests:       4 passed, 4 total
Реальный вывод теста

Те же сценарии мы прошли в браузере на веб-сборке: список, запись, возврат, модальное окно с сохранением, вкладка «About» и прямой заход по адресу /notes/2. На телефоне и эмуляторе урок не запускался — это ваш шаг.

  • Компонент или хук лежит в src/app и появился как лишний маршрут — перенесите его в src/components или src/data
  • router.back() ничего не делает после глубокой ссылки — проверяйте canGoBack() или используйте dismissTo
  • Параметр сравнивают с числом и не находят запись — параметры адреса всегда строки
  • Ссылка со схемой не открывает приложение — после добавления scheme приложение нужно пересобрать
  • tsc не видит типов маршрутов — запустите npx expo start или npx expo customize tsconfig.json

Дальше по траектории React Native: загрузка данных с API и их хранение на устройстве, чтобы записи не пропадали после перезапуска.

Как проверено5 · пройдено1 · не запускалосьсверено 11 октября 2026 г.
  • запущено, работает

    Проект notes-app: src/data/notes.tsx, src/app/_layout.tsx, (tabs)/_layout.tsx, (tabs)/index.tsx, (tabs)/about.tsx, notes/[id].tsx, new.tsx

    create-expo-app@latest --yes (expo 57.0.27, expo-router 57.0.25, react-native 0.86.3, react 19.2.3, typescript 6.0.3) + reset-project + npx expo install @expo/vector-icons (15.1.1); npx expo customize tsconfig.json; tsc --noEmit OK; expo-doctor 21/21; npx expo export --platform web (8 статических маршрутов) / android / ios — бандлы собраны; код статьи считан генератором из файлов проекта

  • запущено, работает

    __tests__/navigation-test.tsx (renderRouter, jest-expo 57.0.5, RNTL 14.1.0)

    npm test: 4 passed — список → запись → назад, глубокая ссылка без истории → назад на /, модальное окно → сохранение → /, неизвестный id; вывод в статье реальный

  • запущено, работает

    Типизированные маршруты (ошибочный href и params)

    временный src/tmp/typed-check.tsx: tsc выдал TS2322 на '/note/1' и на params { noteId }; файл удалён, повторный tsc чистый; вывод в статье реальный, список путей сокращён

  • запущено, работает

    Веб-сборка в браузере (playwright-core + headless Edge, 390×844)

    dist-web на локальном node-сервере (для /notes/* — отдача шаблона notes/[id].html, имитация rewrite): клик по записи → /notes/1 без перезагрузки (метка в window сохранилась), Back → /, кнопка New = <a role=link href=/new> → /new, сохранение → / и новая запись в списке, history back, вкладка About → /about, Link '/notes/1', прямой заход /notes/2 → Back → /. Ошибка гидратации React #418 на прямом заходе — следствие статического экспорта без generateStaticParams и rewrite в тестовом сервере (в статье — пункт про generateStaticParams)

  • запущено, работает

    Упражнения: вкладка Settings, replace после сохранения, поиск через setParams

    решения временно применены в проекте: tsc OK + 3 теста renderRouter (маршрут /settings; /new → сохранение → /notes/<id> → назад → /; ?q=team фильтрует, changeText → params { q: 'call' }); затем исходные файлы восстановлены и полный прогон повторён

  • не запускалось

    Запуск на устройстве/эмуляторе, npx uri-scheme open, Expo Go exp://…/--/, нативные анимации модального окна

    нет устройства и эмулятора на машине; команды и поведение платформ — по документации Expo, в статье явно сказано, что на телефоне урок не запускался

«Запущено» — код выполнен во временном проекте или контейнере, вывод в статье взят из прогона. «Частично» — проверено с заглушками вместо внешних сервисов. «Не запускалось» — нужны устройство, аккаунт или ключ; шаги сверены с официальной документацией. Файлы проверочного проекта и логи запусков сохранены в архиве InterLAB.

Расскажите о задаче — мы изучим запрос и свяжемся с вами

Есть задача?
Обсудим и оценим — бесплатно.

Расскажите о продукте или проблеме. Предложим архитектуру, срок и стоимость первой версии после изучения задачи.

  • Оценка объёма и рисков до старта
  • NDA — по вашему или нашему шаблону
  • Старт работ в течение недели

Не присылайте через форму пароли, ключи доступа и исходный код — безопасную передачу доступа обсудим отдельно.

Нажимая кнопку, вы соглашаетесь с обработкой персональных данных.