Лекция 5. Навигация в мобильных приложениях (React Navigation)
Введение
Любое нетривиальное мобильное приложение состоит из множества экранов: список товаров и карточка товара, лента и профиль, вход и регистрация, настройки. Пользователь постоянно перемещается между ними: открывает детали, возвращается назад, переключается между разделами через нижние вкладки, открывает боковое меню. Всё это — навигация.
В вебе за переходы отвечает сам браузер: есть адресная строка, кнопка «Назад», история. В мобильном приложении ничего этого «из коробки» нет — стек экранов, анимации переходов, аппаратную кнопку «Назад» на Android, заголовки с кнопкой возврата нужно реализовывать самостоятельно. Чтобы не изобретать это каждый раз, в экосистеме React Native используют библиотеку React Navigation — де-факто стандарт навигации.
В этой лекции разберём модель экранов и навигаторов, установку и настройку React Navigation, три основных типа навигаторов (Stack, Tab, Drawer), передачу параметров между экранами, вложенные навигаторы и базовую типобезопасность маршрутов с TypeScript.
1. Зачем нужна навигация: модель экранов и навигаторов
Навигация решает несколько задач одновременно:
- Переходы между экранами — открыть новый экран, вернуться назад.
- Хранение истории — пользователь должен иметь возможность вернуться туда, откуда пришёл.
- Анимации и жесты — слайд вперёд/назад, свайп для возврата, что соответствует ожиданиям пользователей iOS и Android.
- Системная интеграция — аппаратная кнопка «Назад» на Android, заголовки, deep links.
- Передача данных — какой именно товар открыть, какой id передать на следующий экран.
Ключевые понятия в React Navigation:
- Экран (Screen) — обычный React-компонент, который показывается пользователю. Например,
HomeScreen,DetailsScreen. - Навигатор (Navigator) — компонент, который управляет группой экранов: хранит, какой экран активен, ведёт историю, рисует заголовки и переходы. Каждый навигатор задаёт свою модель перемещения (стек, вкладки, меню).
- NavigationContainer — корневой контейнер, который оборачивает всё дерево навигаторов и связывает его с состоянием приложения и системой.
- Объект
navigation— передаётся в каждый экран и позволяет инициировать переходы (navigate,goBack,pushи т. д.). - Объект
route— описывает текущий маршрут: его имя (route.name) и переданные параметры (route.params).
Важно понимать: навигаторы можно вкладывать друг в друга. Реальные приложения почти всегда комбинируют несколько типов — например, нижние вкладки (Tab), внутри каждой из которых живёт свой стек (Stack).
2. Установка и базовая настройка
React Navigation состоит из ядра и отдельных пакетов под каждый тип навигатора. Базовая установка:
# Ядро библиотекиnpm install @react-navigation/native
# Обязательные зависимости (в Expo лучше через expo install)npx expo install react-native-screens react-native-safe-area-context
# Навигаторы — ставим те, что нужныnpm install @react-navigation/native-stack # Stacknpm install @react-navigation/bottom-tabs # Tabnpm install @react-navigation/drawer # Drawer
# Для Drawer дополнительно нужны жесты и анимацииnpx expo install react-native-gesture-handler react-native-reanimatedМинимальная рабочая настройка состоит из двух обязательных частей: корневого NavigationContainer и хотя бы одного навигатора внутри него.
import { NavigationContainer } from '@react-navigation/native';import { createNativeStackNavigator } from '@react-navigation/native-stack';import HomeScreen from './src/screens/HomeScreen';import DetailsScreen from './src/screens/DetailsScreen';
const Stack = createNativeStackNavigator();
export default function App() { return ( <NavigationContainer> <Stack.Navigator initialRouteName="Home"> <Stack.Screen name="Home" component={HomeScreen} /> <Stack.Screen name="Details" component={DetailsScreen} /> </Stack.Navigator> </NavigationContainer> );}Здесь NavigationContainer оборачивает приложение и должен быть единственным на верхнем уровне. Stack.Navigator хранит экраны, а каждый Stack.Screen связывает строковое имя маршрута (name) с компонентом (component).
3. Типы навигаторов
3.1. Stack — стек экранов (push/pop)
Stack-навигатор работает по принципу стека (LIFO): новый экран кладётся поверх предыдущего, при возврате снимается верхний. Это основной паттерн для иерархических переходов: список → детали → ещё детали.
navigation.navigate('Details', { ... })— перейти к экрану. Если экран уже есть в стеке, фокус вернётся к нему; если нет — он добавится.navigation.push('Details', { ... })— всегда добавить новый экземпляр экрана сверху (можно открывать «детали деталей»).navigation.goBack()— вернуться на предыдущий экран (снять верхний).navigation.popToTop()— вернуться к самому первому экрану стека.
Stack автоматически рисует заголовок (header) с кнопкой «Назад» и поддерживает свайп для возврата на iOS.
import { createNativeStackNavigator } from '@react-navigation/native-stack';import { View, Text, Button } from 'react-native';
const Stack = createNativeStackNavigator();
function HomeScreen({ navigation }) { return ( <View> <Text>Главный экран</Text> <Button title="Открыть детали" onPress={() => navigation.navigate('Details', { id: 42 })} /> </View> );}
function DetailsScreen({ route, navigation }) { const { id } = route.params; return ( <View> <Text>Детали элемента #{id}</Text> <Button title="Назад" onPress={() => navigation.goBack()} /> </View> );}
export function AppStack() { return ( <Stack.Navigator> <Stack.Screen name="Home" component={HomeScreen} options={{ title: 'Главная' }} /> <Stack.Screen name="Details" component={DetailsScreen} /> </Stack.Navigator> );}3.2. Tab — нижние вкладки
Tab-навигатор показывает панель вкладок (обычно внизу экрана) и переключает между разделами одного уровня. В отличие от стека, вкладки равноправны и не образуют истории «вперёд/назад» — пользователь просто переключается между параллельными разделами: «Лента», «Поиск», «Профиль».
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';import { Ionicons } from '@expo/vector-icons';import FeedScreen from './src/screens/FeedScreen';import ProfileScreen from './src/screens/ProfileScreen';
const Tab = createBottomTabNavigator();
export function MainTabs() { return ( <Tab.Navigator screenOptions={{ tabBarActiveTintColor: '#2563eb' }} > <Tab.Screen name="Feed" component={FeedScreen} options={{ title: 'Лента', tabBarIcon: ({ color, size }) => ( <Ionicons name="home" color={color} size={size} /> ), }} /> <Tab.Screen name="Profile" component={ProfileScreen} options={{ title: 'Профиль' }} /> </Tab.Navigator> );}Важная особенность: при переключении вкладок экраны обычно не размонтируются, а сохраняют своё состояние — это удобно для лент и форм.
3.3. Drawer — боковое меню
Drawer-навигатор открывает выдвижную панель сбоку (по свайпу от края или по нажатию на «бургер»). Подходит для приложений с большим числом разделов, которые не помещаются в нижние вкладки: настройки, разделы каталога, дополнительные функции.
import { createDrawerNavigator } from '@react-navigation/drawer';import HomeScreen from './src/screens/HomeScreen';import SettingsScreen from './src/screens/SettingsScreen';
const Drawer = createDrawerNavigator();
export function AppDrawer() { return ( <Drawer.Navigator> <Drawer.Screen name="Home" component={HomeScreen} options={{ title: 'Главная' }} /> <Drawer.Screen name="Settings" component={SettingsScreen} options={{ title: 'Настройки' }} /> </Drawer.Navigator> );}Открыть меню программно можно через navigation.openDrawer(), закрыть — navigation.closeDrawer().
3.4. Какой навигатор когда использовать
| Навигатор | Модель перемещения | Когда использовать | Примеры |
|---|---|---|---|
| Stack | Стек (push/pop), история «вперёд/назад» | Иерархические переходы: от общего к частному | Список → карточка товара → отзывы |
| Tab | Параллельные равноправные разделы | 2–5 основных разделов приложения | Лента / Поиск / Профиль |
| Drawer | Боковое выдвижное меню | Много разделов, второстепенная навигация | Настройки, разделы каталога, «О приложении» |
На практике их комбинируют: например, Tab на верхнем уровне, а внутри каждой вкладки — свой Stack. Не нужно выбирать что-то одно.
4. Передача параметров между экранами
4.1. Передача параметров вперёд
Параметры передаются вторым аргументом в navigate (или push) и доступны на целевом экране через route.params.
// Экран-источникnavigation.navigate('Details', { id: 42, title: 'Книга' });
// Экран-получатель (DetailsScreen)function DetailsScreen({ route }) { const { id, title } = route.params; return <Text>{title} (#{id})</Text>;}Рекомендации по параметрам:
- Передавайте минимум данных — обычно идентификатор (
id), а сам объект подгружайте на целевом экране. Это упрощает deep links и не дублирует данные. - Параметры должны быть сериализуемыми (числа, строки, простые объекты). Не передавайте функции и сложные инстансы.
4.2. Значения по умолчанию
Если экран может открываться без параметров, задайте значения по умолчанию через initialParams или при деструктуризации:
function DetailsScreen({ route }) { const { id = 0 } = route.params ?? {}; // ...}4.3. Возврат данных на предыдущий экран
Иногда нужно вернуть результат с дочернего экрана (например, выбранный пункт из экрана-селектора). Самый простой подход — передать данные обратно через параметры предыдущего маршрута с помощью navigation.navigate, указав имя экрана-получателя:
// Экран Home открывает экран выбора городаnavigation.navigate('SelectCity');
// Экран SelectCity возвращает результат на экран Homefunction SelectCityScreen({ navigation }) { function choose(city) { navigation.navigate('Home', { selectedCity: city }); } // ...}
// Экран Home читает результатfunction HomeScreen({ route }) { const city = route.params?.selectedCity; // реагируем на изменение, например через useEffect}Альтернатива при возврате результата — обновить параметры текущего экрана через navigation.setParams({ ... }) или использовать общий стор/контекст для обмена данными между экранами.
5. Вложенные навигаторы и организация графа экранов
Реальные приложения комбинируют навигаторы: один навигатор становится экраном другого. Классический пример — нижние вкладки, внутри каждой из которых свой стек, плюс «модальные» экраны поверх всего на уровне корневого стека.
import { NavigationContainer } from '@react-navigation/native';import { createNativeStackNavigator } from '@react-navigation/native-stack';import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
const RootStack = createNativeStackNavigator();const Tab = createBottomTabNavigator();
function Tabs() { return ( <Tab.Navigator> <Tab.Screen name="Feed" component={FeedScreen} /> <Tab.Screen name="Profile" component={ProfileScreen} /> </Tab.Navigator> );}
export default function App() { return ( <NavigationContainer> <RootStack.Navigator> {/* Таб-навигатор как корневой экран — заголовок прячем */} <RootStack.Screen name="Root" component={Tabs} options={{ headerShown: false }} /> {/* Экран поверх вкладок (например, модальный) */} <RootStack.Screen name="Details" component={DetailsScreen} /> </RootStack.Navigator> </NavigationContainer> );}Разделение стеков: auth-stack и app-stack (вводно)
Частая задача — показывать разные наборы экранов в зависимости от того, авторизован пользователь или нет:
- Auth-stack — экраны входа, регистрации, восстановления пароля. Доступны неавторизованному пользователю.
- App-stack — основной функционал приложения (вкладки, экраны), доступный после входа.
Идиоматичный подход в React Navigation — условный рендеринг навигаторов на основе состояния авторизации, а не ручные переходы. Это автоматически и безопасно переключает граф экранов:
function RootNavigator() { const { token } = useAuth(); // токен из контекста/стора
return ( <NavigationContainer> {token ? <AppStack /> : <AuthStack />} </NavigationContainer> );}Когда token появляется (после входа) — дерево перестраивается на AppStack; когда исчезает (выход) — на AuthStack. Пользователь не сможет «вернуться назад» на защищённые экраны, потому что их просто нет в дереве. Подробно защиту маршрутов и флоу аутентификации разберём в лекции 11.
6. Типобезопасность маршрутов (TypeScript, кратко)
По умолчанию имена экранов и их параметры — обычные строки и объекты, и ошибку (опечатку в имени, забытый параметр) можно поймать только в рантайме. TypeScript позволяет описать карту параметров для каждого навигатора и получать проверки на этапе компиляции и автодополнение.
// Описываем экраны и их параметрыtype RootStackParamList = { Home: undefined; // без параметров Details: { id: number }; // обязательный id};
const Stack = createNativeStackNavigator<RootStackParamList>();
// Типизируем props экранаimport { NativeStackScreenProps } from '@react-navigation/native-stack';
type Props = NativeStackScreenProps<RootStackParamList, 'Details'>;
function DetailsScreen({ route }: Props) { const { id } = route.params; // id выведен как number return <Text>{id}</Text>;}Теперь navigation.navigate('Details', { id: 42 }) проверяется компилятором: нельзя ошибиться в имени маршрута или забыть/перепутать тип параметра. Это особенно ценно в больших проектах с десятками экранов.
Краткие итоги
- Навигация в мобильных приложениях не предоставляется «из коробки» — её обеспечивает библиотека React Navigation.
- Базовая модель: экраны (компоненты) живут внутри навигаторов, всё дерево оборачивается единственным NavigationContainer.
- Три основных навигатора: Stack (иерархия, push/pop, кнопка «Назад»), Tab (равноправные разделы, нижние вкладки), Drawer (боковое меню для второстепенной навигации). На практике их комбинируют.
- Параметры передаются через
navigation.navigate('Screen', params)и читаются черезroute.params; передавать стоит минимум данных (обычноid). Возврат данных — черезnavigateобратно или общий стор. - Вложенные навигаторы позволяют строить сложный граф экранов; разделение на auth-stack и app-stack делают условным рендерингом по состоянию авторизации.
- TypeScript даёт типобезопасность маршрутов: проверку имён и параметров на этапе компиляции.
Вопросы для самопроверки
- (Вопрос 12) Опишите основные навигационные паттерны в React Navigation: Stack, Tab, Drawer. Когда использовать каждый из них?
- (Вопрос 13) Как передавать параметры между экранами в React Navigation? Как вернуть данные на предыдущий экран?
- (Вопрос 28) Как организовать граф экранов в приложении? Опишите разделение стеков (auth-stack, app-stack).
- Зачем нужен
NavigationContainerи почему он должен быть один? - В чём разница между
navigation.navigateиnavigation.push? - Как и зачем вкладывают навигаторы друг в друга? Приведите пример Tab + Stack.
- Какие преимущества даёт типизация маршрутов в TypeScript?