Навигация в 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