React Router: программная навигация без боли и тормозов

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Навигация в React: не просто клик по ссылке

Вы когда-нибудь задумывались, как заставить React Router перейти на другую страницу не по клику пользователя, а по команде из кода? Например, после успешной отправки формы, авторизации или просто через 5 секунд? Если да, то вы попали по адресу. Вопрос "How can I programmatically navigate using React Router?" — один из самых популярных на Stack Overflow, и неспроста. Кажется, что это просто, но на деле здесь скрывается множество подводных камней, особенно когда речь заходит о производительности.

В этой статье мы не просто покажем, как использовать useNavigate. Мы разберем, как делать это правильно, быстро и без лагов. Вы узнаете, какие паттерны ведут к утечкам памяти, а какие — к молниеносной навигации. Поехали!

Классика: useNavigate — ваш главный инструмент

Начиная с React Router v6, useNavigate заменил старый добрый useHistory. Это хук, который возвращает функцию navigate. Выглядит это так:

import { useNavigate } from 'react-router-dom';

function LoginButton() {
  const navigate = useNavigate();

  const handleLogin = () => {
    // Тут логика авторизации
    const user = { name: 'Иван' };
    // После успеха — редирект
    navigate('/dashboard', { replace: true });
  };

  return <button onClick={handleLogin}>Войти</button>;
}

Обратите внимание на параметр { replace: true }. Он заменяет текущую запись в истории, а не добавляет новую. Это критично для логина, чтобы пользователь не вернулся назад на страницу входа.

Куда можно переходить?

  • Абсолютный путь: navigate('/about')
  • Относительный путь: navigate('../settings')
  • Назад/вперед по истории: navigate(-1) или navigate(2)

Продвинутый уровень: навигация за пределами компонентов

А что, если вам нужно сделать редирект из обычной функции, не внутри React-компонента? Например, из action creator в Redux или из axios-интерцептора? Хук useNavigate здесь не сработает — хуки вызываются только внутри функциональных компонентов.

Решение: создать кастомный компонент-обертку или использовать глобальный объект маршрутизатора. В React Router v6 для этого есть специальный компонент RouterProvider и createBrowserRouter, но есть и более простой способ — сохранить ссылку на navigate в переменной.

// router.js
import { createBrowserRouter, RouterProvider } from 'react-router-dom';

export const router = createBrowserRouter([...]);

// navigation.js — отдельный модуль для навигации
let navigateFunction = null;

export const setNavigate = (navigate) => {
  navigateFunction = navigate;
};

export const navigateTo = (path) => {
  if (navigateFunction) {
    navigateFunction(path);
  } else {
    console.warn('Навигация еще не готова');
  }
};

// App.js
import { useNavigate } from 'react-router-dom';
import { setNavigate } from './navigation';
import { useEffect } from 'react';

function App() {
  const navigate = useNavigate();

  useEffect(() => {
    setNavigate(navigate);
  }, [navigate]);

  return /* ваш код */;
}

Теперь из любой функции (хоть из setTimeout, хоть из fetch) вы можете вызвать navigateTo('/home'). Удобно? Да. Безопасно? Только если вы не забудете очистить ссылку при размонтировании компонента.

Оптимизация производительности: почему ваше приложение тормозит?

Программная навигация — это не просто вызов функции. Это триггер для полного ререндера страницы. И если вы делаете это неправильно, ваше приложение может начать "подтормаживать". Вот основные грабли:

1. Навигация внутри useEffect без зависимостей

Это классика. Вы пишете что-то вроде:

useEffect(() => {
  navigate('/dashboard');
});

И получаете бесконечный цикл редиректов. Почему? Потому что useEffect без массива зависимостей запускается после каждого рендера. Вы переходите на /dashboard, компонент рендерится снова, хук срабатывает опять — и так до бесконечности. Решение: всегда указывайте массив зависимостей или используйте useEffect с пустым массивом [], если редирект нужен только один раз при монтировании.

2. Устаревшие замыкания (Stale Closures)

Представьте: у вас есть кнопка "Перейти к товару". Вы передаете в обработчик onClick функцию, которая использует navigate. Все работает. Но потом вы добавляете debounce или throttle, и вдруг навигация перестает работать. Причина: замыкание "запомнило" старую версию navigate. Решение: используйте useCallback для стабилизации ссылки на функцию.

const handleRedirect = useCallback(() => {
  navigate('/product/123');
}, [navigate]); // navigate стабилен, не пересоздается

3. Избыточные ререндеры из-за контекста маршрутизатора

React Router использует React.Context для передачи данных о маршруте. Если вы вызываете navigate из компонента, который не вложен в <Router>, вы получите ошибку. Но если ваш компонент слишком часто перерисовывается из-за контекста, это может ударить по производительности. Используйте React.memo для компонентов, которые не зависят от маршрута, и старайтесь не вызывать navigate в глубоко вложенных компонентах без необходимости.

Практические кейсы: когда навигация жизненно необходима

Сценарий 1: Редирект после логина

Самый частый случай. Пользователь ввел данные, нажал "Войти", и мы должны перенаправить его в личный кабинет. Но есть нюанс: если запрос на сервер еще не завершился, а мы уже делаем редирект, то пользователь попадет на пустую страницу. Правильный подход:

const handleSubmit = async (event) => {
  event.preventDefault();
  setIsLoading(true);
  try {
    const response = await loginApi(formData);
    if (response.success) {
      navigate('/dashboard', { replace: true });
    }
  } catch (error) {
    setError(error.message);
  } finally {
    setIsLoading(false);
  }
};

Сценарий 2: Таймер обратного отсчета

Вы показываете пользователю сообщение "Вы будете перенаправлены через 5 секунд". Тут нужно быть аккуратным с очисткой таймера:

const [countdown, setCountdown] = useState(5                

Блоги

Рекомендаций по книгам