React Navigation: стек, табы, параметры и deep links

Навигация — обязательный блок вопросов: в React Native она не встроена и живёт отдельной библиотекой.

React Navigation — стандарт де-факто для навигации в RN. Экраны организованы в навигаторы (стек, табы, drawer), а их текущее состояние — обычное сериализуемое дерево.

Вопрос

«Как устроена навигация в React Native? Чем стековый навигатор отличается от табового, как передавать параметры и как обновить данные при возврате на экран?»

Что на самом деле проверяет интервьюер

Понимаете ли вы, что предыдущие экраны остаются живыми, и умеете ли работать с фокусом. Это источник двух вечных багов: «данные не обновились после редактирования» и «на экране в фоне продолжает крутиться таймер».

Развёрнутый ответ

Минимальная схема

import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';

const Stack = createNativeStackNavigator();
const Tab = createBottomTabNavigator();

function FeedStack() {
  return (
    <Stack.Navigator>
      <Stack.Screen name="Feed" component={FeedScreen} options={{ title: 'Лента' }} />
      <Stack.Screen name="Post" component={PostScreen} />
    </Stack.Navigator>
  );
}

export default function App() {
  return (
    <NavigationContainer>
      <Tab.Navigator>
        <Tab.Screen name="FeedTab" component={FeedStack} options={{ title: 'Лента' }} />
        <Tab.Screen name="ProfileTab" component={ProfileScreen} options={{ title: 'Профиль' }} />
      </Tab.Navigator>
    </NavigationContainer>
  );
}

Полезно отметить: native-stack использует нативные контроллеры навигации платформы, поэтому переходы и жесты ощущаются «родными», в отличие от старого JS-стека.

Параметры маршрута

// откуда переходим
navigation.navigate('Post', { postId: 42 });

// куда переходим
function PostScreen({ route, navigation }) {
  const { postId } = route.params;
  // ...
}

// вернуть результат назад — не колбэком, а через параметры предыдущего экрана
navigation.navigate('Feed', { createdPostId: newId });

Правило: в параметрах — только сериализуемые примитивы, прежде всего идентификаторы. Состояние навигации может сохраняться и восстанавливаться, поэтому функции и большие объекты туда класть нельзя.

navigate против push и как вернуться

  • navigate('Post') — перейти к экрану; если такой уже есть в стеке, вернуться к нему.
  • push('Post') — всегда положить сверху новый экземпляр (цепочка «товар — похожий товар»).
  • goBack() — на шаг назад, popToTop() — в корень стека.
  • replace('Home') — заменить текущий экран (типично после логина, чтобы нельзя было вернуться назад).
  • reset(...) — полностью перестроить стек, например при выходе из аккаунта.

Экраны живут в фоне

import { useFocusEffect } from '@react-navigation/native';
import { useCallback } from 'react';

useFocusEffect(
  useCallback(() => {
    const timer = setInterval(refresh, 30000);
    return () => clearInterval(timer); // при уходе с экрана таймер гасим
  }, [refresh])
);

Есть и хук useIsFocused(), если нужно просто не рендерить тяжёлое содержимое на неактивном экране. Отдельно стоит упомянуть unmountOnBlur у табов — им лечат «висящие» экраны.

Deep links

Глубокие ссылки нужны для писем, push-уведомлений и рекламных кампаний. Конфигурация связывает URL с маршрутами:

const linking = {
  prefixes: ['myapp://', 'https://example.com'],
  config: {
    screens: {
      FeedTab: {
        screens: {
          Feed: 'feed',
          Post: 'post/:postId',
        },
      },
      ProfileTab: 'profile',
    },
  },
};

<NavigationContainer linking={linking} fallback={<Splash />}>...</NavigationContainer>

На стороне платформ нужно ещё зарегистрировать схему и домены: на iOS — URL types и Associated Domains для universal links, на Android — intent-filter с autoVerify для App Links. Проверяют так:

npx uri-scheme open "myapp://post/42" --ios
adb shell am start -W -a android.intent.action.VIEW -d "myapp://post/42" com.example.myapp

Типизация в TypeScript

export type RootStackParamList = {
  Feed: undefined;
  Post: { postId: number };
};

// теперь навигация и route.params проверяются компилятором
const navigation = useNavigation<NativeStackNavigationProp<RootStackParamList>>();

Типичные ошибки кандидатов

  • Передают колбэк через параметры маршрута — это ломает восстановление состояния и вызывает предупреждение.
  • Используют useEffect с пустым массивом там, где нужен useFocusEffect: экран не размонтировался, эффект не повторится.
  • Не различают navigate и push, из-за чего цепочка похожих экранов «схлопывается».
  • Думают, что deep link настраивается только в JS, забывая про схемы в Info.plist и intent-filter в манифесте.

Как ответить кратко

«Навигация — это React Navigation: NavigationContainer сверху, внутри навигаторы — native-stack, табы, drawer. Параметры передаю через navigate с сериализуемыми данными, только id, никаких функций и больших объектов; результат возвращаю параметрами предыдущего экрана. navigate переходит к существующему экрану, push всегда добавляет новый; replace и reset — для логина и выхода. Важно, что экраны остаются смонтированными, поэтому обновление и таймеры вешаю на useFocusEffect. Deep links описываю конфигом linking, плюс регистрирую схему в Info.plist и intent-filter в манифесте.»

Проверьте себя
1. Чем navigation.navigate('Details') отличается от navigation.push('Details')?
AНичем, push — устаревший псевдоним navigate
Bnavigate вернётся к существующему экрану с такими же параметрами, а push всегда добавит новый экран в стек
Cpush работает только на Android
Dnavigate заменяет текущий экран, push открывает модально
2. Почему useEffect с пустым массивом зависимостей не подходит для «обновить данные при возврате на экран»?
AПотому что экран при возврате не размонтируется — он остаётся в стеке, и эффект больше не запускается
BПотому что useEffect не работает внутри навигаторов
CПотому что React Navigation отключает эффекты на неактивных экранах
DПотому что при возврате компонент монтируется заново и эффект сработает дважды
3. Что не стоит передавать через параметры маршрута?
AИдентификатор сущности
BБольшие объекты и функции обратного вызова
CКороткую строку с названием
DЧисло — например, номер страницы