Навігація та корисні хуки React Router
Навігація та корисні хуки React Router
🔗 Офіційний референс: React Router v7 - Navigating
🎯 Мета лекції
- Опанувати програмну навігацію через хук
useNavigate. - Створювати активні посилання навігації через компонент
NavLink. - Працювати з пошуковими параметрами URL через
useSearchParams. - Відстежувати зміни локації через
useLocation. - Інтегрувати React Router з історією браузера (кнопки Назад/Вперед).
🔑 Ключові терміни
- useNavigate: хук для програмної навігації без взаємодії користувача.
- NavLink: компонент посилання з автоматичним додаванням класу
.activeдля активних маршрутів. - Search Params (Query Params): параметри після
?у URL (?sort=name&page=2). - useLocation: хук для доступу до поточного об'єкта локації (
pathname,search,hash,state,key).
Програмна навігація: useNavigate
У попередніх лекціях ми використовували компоненти <Link> та <NavLink> для навігації, що вимагають взаємодії користувача (клік). Проте часто потрібно перенаправити користувача програмно — без його участі:
Use Case 1: Після успішного сабміту форми
import { useNavigate } from "react-router";
function LoginForm() {
const navigate = useNavigate();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
// ... валідація та відправка на сервер
const success = await login(email, password);
if (success) {
navigate("/dashboard"); // Редирект після успішного логіну
}
};
return <form onSubmit={handleSubmit}>{/* Поля форми */}</form>;
}
Use Case 2: Автоматичний редирект після таймауту
import { useNavigate } from "react-router";
import { useEffect } from "react";
function LogoutPage() {
const navigate = useNavigate();
useEffect(() => {
// Видаляємо токен
localStorage.removeItem("authToken");
// Через 3 секунди редиректимо на логін
const timer = setTimeout(() => {
navigate("/login");
}, 3000);
return () => clearTimeout(timer);
}, [navigate]);
return (
<div>
<h1>Ви вийшли з системи</h1>
<p>Перенаправлення на сторінку входу через 3 секунди...</p>
</div>
);
}
Use Case 3: Захищені маршрути
import { useNavigate } from "react-router";
import { useEffect } from "react";
function ProtectedPage() {
const navigate = useNavigate();
const isAuthenticated = checkAuth(); // Перевірка авторизації
useEffect(() => {
if (!isAuthenticated) {
navigate("/login", { replace: true });
}
}, [isAuthenticated, navigate]);
if (!isAuthenticated) {
return null; // Або спінер
}
return <div>Захищений контент</div>;
}
useNavigate() повертає функцію, яку можна викликати з двома аргументами:navigate(to: string | number, options?: NavigateOptions)
to: шлях для навігації ("/dashboard") або число для навігації по історії (-1= назад,1= вперед).options:replace: boolean— замінити поточний запис в історії замість додавання нового (користувач не зможе повернутись кнопкою "Назад").state: any— передати стан разом з навігацією (доступний черезuseLocation().state).preventScrollReset: boolean— запобігти автоматичному скролу вгору після навігації.
Навігація по історії браузера
useNavigate дозволяє переміщатися по історії відносно поточної позиції:
const navigate = useNavigate();
// Повернутись на одну сторінку назад (еквівалент кнопки "Назад")
navigate(-1);
// Повернутись на дві сторінки назад
navigate(-2);
// Перейти на одну сторінку вперед (еквівалент кнопки "Вперед")
navigate(1);
Це корисно для кнопок "Назад" у застосунку:
function BackButton() {
const navigate = useNavigate();
return (
<button onClick={() => navigate(-1)}>
← Назад
</button>
);
}
navigate("/path") та navigate(-1):navigate("/path")— абсолютна навігація до конкретного маршруту.navigate(-1)— відносна навігація по історії браузера.
navigate(-1) поверне його на попередній сайт, а не на попередню сторінку вашого застосунку!Для безпеки краще використовувати абсолютні шляхи або перевіряти, чи є попередня сторінка всередині вашого застосунку.Опція replace: заміна запису в історії
За замовчуванням навігація додає новий запис в історію браузера. Користувач може натиснути "Назад" і повернутись на попередню сторінку.
Проте іноді потрібно замінити поточний запис, щоб користувач не міг повернутись на цю сторінку:
// Редирект після логіну — користувач не має повертатись на сторінку логіну кнопкою "Назад"
navigate("/dashboard", { replace: true });
// Редирект з 404 — користувач не має повертатись на неіснуючу сторінку
navigate("/", { replace: true });
replace: true:- Після успішної авторизації (щоб кнопка "Назад" не вела на форму логіну).
- При редиректі з 404 або помилки.
- При програмному редиректі, який не є результатом дії користувача.
- Для звичайної навігації між сторінками (користувач має мати змогу повернутись).
- Після сабміту форми, коли користувач може захотіти повернутись для редагування.
Передача стану через навігацію
Іноді потрібно передати дані разом з навігацією без додавання їх до URL:
// Сторінка списку користувачів
function UsersList() {
const navigate = useNavigate();
const handleViewUser = (user: User) => {
navigate(`/users/${user.id}`, {
state: { userName: user.name, fromList: true },
});
};
return (
<ul>
{users.map((user) => (
<li key={user.id}>
<button onClick={() => handleViewUser(user)}>
{user.name}
</button>
</li>
))}
</ul>
);
}
// Сторінка деталей користувача
function UserDetails() {
const { userId } = useParams();
const location = useLocation();
// Витягуємо стан, переданий через navigate
const state = location.state as { userName?: string; fromList?: boolean };
return (
<div>
<h1>Деталі користувача {userId}</h1>
{state?.fromList && (
<p>Ви перейшли зі списку користувачів. Ім'я: {state.userName}</p>
)}
</div>
);
}
location.state зберігається в історії браузера, але не відображається в URL. Це корисно для передачі контекстної інформації, яка не має бути видимою користувачу або шарабельною.Проте, якщо користувач оновить сторінку (F5), location.stateбуде втрачено. Для персистентних даних використовуйте URL-параметри або localStorage.Активні посилання: NavLink
NavLink — це спеціалізована версія Link, що автоматично додає клас .active до активного посилання:
import { NavLink } from "react-router";
function Navigation() {
return (
<nav>
<NavLink to="/">Головна</NavLink>
<NavLink to="/about">Про нас</NavLink>
<NavLink to="/contact">Контакти</NavLink>
</nav>
);
}
Коли URL збігається з to, React Router додає клас .active:
/* styles.css */
nav a {
color: black;
text-decoration: none;
margin-right: 1rem;
}
nav a.active {
color: #3b82f6;
font-weight: bold;
border-bottom: 2px solid #3b82f6;
}
Динамічні класи та стилі через callback
Для більш складної логіки NavLink приймає callback для className, style та children:
function Navigation() {
return (
<nav>
<NavLink
to="/"
className={({ isActive }) =>
isActive ? "nav-link nav-link-active" : "nav-link"
}
>
Головна
</NavLink>
<NavLink
to="/about"
style={({ isActive }) => ({
color: isActive ? "red" : "black",
fontWeight: isActive ? "bold" : "normal",
})}
>
Про нас
</NavLink>
<NavLink to="/contact">
{({ isActive }) => (
<span className={isActive ? "active" : ""}>
{isActive ? "👉 " : ""}
Контакти
</span>
)}
</NavLink>
</nav>
);
}
end: за замовчуванням NavLink вважається активним, якщо URL починається з to. Це означає, що / буде активним для всіх маршрутів (/, /about, /contact).Щоб активувати посилання лише за точного збігу, використовуйте end:<NavLink to="/" end>
Головна
</NavLink>
/ активний лише на головній сторінці, а не на /about чи /contact.Пошукові параметри: useSearchParams
Пошукові параметри (query params) — це частина URL після ?:
/products?sort=name&category=electronics&page=2
React Router надає хук useSearchParams для роботи з ними:
import { useSearchParams } from "react-router";
function ProductsList() {
const [searchParams, setSearchParams] = useSearchParams();
// Читання параметрів
const sort = searchParams.get("sort") || "name";
const category = searchParams.get("category") || "all";
const page = parseInt(searchParams.get("page") || "1", 10);
// Зміна параметрів
const handleSortChange = (newSort: string) => {
setSearchParams({ sort: newSort, category, page: page.toString() });
};
return (
<div>
<h1>Продукти</h1>
<p>Сортування: {sort}, Категорія: {category}, Сторінка: {page}</p>
<button onClick={() => handleSortChange("price")}>
Сортувати за ціною
</button>
<button onClick={() => handleSortChange("name")}>
Сортувати за назвою
</button>
</div>
);
}
useSearchParams повертає кортеж[searchParams, setSearchParams], схожий на useState:searchParams: об'єкт типуURLSearchParamsз методами.get(),.has(),.getAll(),.entries().setSearchParams: функція для оновлення параметрів. Приймає об'єкт або функцію(prev) => newParams.
setSearchParams((prev) => {
const newParams = new URLSearchParams(prev);
newParams.set("page", "2");
return newParams;
});
Типізація пошукових параметрів
URLSearchParams не має типізації для конкретних ключів. Для строгої типобезпеки можна створити кастомний хук:
import { useSearchParams } from "react-router";
type ProductsSearchParams = {
sort?: "name" | "price" | "date";
category?: string;
page?: number;
};
function useTypedSearchParams() {
const [searchParams, setSearchParams] = useSearchParams();
const params: ProductsSearchParams = {
sort: (searchParams.get("sort") as ProductsSearchParams["sort"]) || "name",
category: searchParams.get("category") || undefined,
page: parseInt(searchParams.get("page") || "1", 10),
};
const updateParams = (newParams: Partial<ProductsSearchParams>) => {
const updated = new URLSearchParams(searchParams);
if (newParams.sort) updated.set("sort", newParams.sort);
if (newParams.category) updated.set("category", newParams.category);
if (newParams.page) updated.set("page", newParams.page.toString());
setSearchParams(updated);
};
return [params, updateParams] as const;
}
// Використання
function ProductsList() {
const [params, updateParams] = useTypedSearchParams();
return (
<div>
<p>Сортування: {params.sort}</p>
<button onClick={() => updateParams({ sort: "price" })}>
За ціною
</button>
</div>
);
}
Відстеження локації: useLocation
useLocation повертає об'єкт поточної локації:
import { useLocation } from "react-router";
function CurrentPath() {
const location = useLocation();
return (
<div>
<p>Шлях: {location.pathname}</p>
<p>Пошук: {location.search}</p>
<p>Хеш: {location.hash}</p>
<p>Ключ: {location.key}</p>
<p>Стан: {JSON.stringify(location.state)}</p>
</div>
);
}
Поля location:
pathname: шлях URL без domain (/products/123).search: пошукові параметри (?sort=name&page=2).hash: фрагмент URL (#section-1).state: стан, переданий черезnavigate(path, { state }).key: унікальний ключ локації (змінюється при навігації).
Практичне завдання
Створіть сторінку списку товарів з фільтрацією через пошукові параметри:
- Фільтри: категорія, діапазон цін, сортування.
- При зміні фільтра оновлюйте URL (не перезавантажуючи сторінку).
- При завантаженні сторінки зчитуйте фільтри з URL.
- Кнопка "Скинути фільтри" має очистити всі параметри.
import { useSearchParams, Link } from "react-router";
type Product = {
id: number;
name: string;
category: string;
price: number;
};
const mockProducts: Product[] = [
{ id: 1, name: "Ноутбук", category: "electronics", price: 25000 },
{ id: 2, name: "Телефон", category: "electronics", price: 15000 },
{ id: 3, name: "Книга", category: "books", price: 300 },
{ id: 4, name: "Навушники", category: "electronics", price: 2000 },
];
function ProductsFilterPage() {
const [searchParams, setSearchParams] = useSearchParams();
const category = searchParams.get("category") || "all";
const minPrice = parseInt(searchParams.get("minPrice") || "0", 10);
const maxPrice = parseInt(searchParams.get("maxPrice") || "100000", 10);
const sort = searchParams.get("sort") || "name";
// Фільтрація
const filteredProducts = mockProducts
.filter((p) => category === "all" || p.category === category)
.filter((p) => p.price >= minPrice && p.price <= maxPrice)
.sort((a, b) => {
if (sort === "price") return a.price - b.price;
return a.name.localeCompare(b.name);
});
const updateFilter = (key: string, value: string) => {
const newParams = new URLSearchParams(searchParams);
newParams.set(key, value);
setSearchParams(newParams);
};
const resetFilters = () => {
setSearchParams({});
};
return (
<div>
<h1>Каталог товарів</h1>
<div style={{ marginBottom: "1rem" }}>
<label>
Категорія:
<select
value={category}
onChange={(e) => updateFilter("category", e.target.value)}
>
<option value="all">Усі</option>
<option value="electronics">Електроніка</option>
<option value="books">Книги</option>
</select>
</label>
<label style={{ marginLeft: "1rem" }}>
Сортування:
<select
value={sort}
onChange={(e) => updateFilter("sort", e.target.value)}
>
<option value="name">За назвою</option>
<option value="price">За ціною</option>
</select>
</label>
<button onClick={resetFilters} style={{ marginLeft: "1rem" }}>
Скинути фільтри
</button>
</div>
<div>
<p>Знайдено товарів: {filteredProducts.length}</p>
<ul>
{filteredProducts.map((product) => (
<li key={product.id}>
<Link to={`/products/${product.id}`}>
{product.name} — {product.price} ₴
</Link>
</li>
))}
</ul>
</div>
</div>
);
}
export default ProductsFilterPage;
Пояснення:
- Фільтри зберігаються в URL як пошукові параметри.
- При зміні фільтра викликається
updateFilter, що оновлює URL. - React Router автоматично викликає ререндер при зміні URL.
- Користувач може поширити посилання з фільтрами — отримувач побачить той самий набір товарів.
Підсумок
✅ Що ми опанували
- Програмну навігацію через
useNavigate()без взаємодії користувача. - Активні посилання через
<NavLink>з автоматичним класом.active. - Роботу з пошуковими параметрами через
useSearchParams. - Відстеження поточної локації через
useLocation. - Передачу стану через
navigate(path, { state })таlocation.state.
🎯 Ключові практики
useNavigateдля програмної навігації,Link/NavLinkдля взаємодії користувача.replace: trueдля редиректів, де користувач не має повертатись назад.useSearchParamsдля стану, що має бути шарабельним та букмаркабельним.location.stateдля тимчасового контексту, що не має відображатись в URL.
Динамічні маршрути та параметри URL
Робота з динамічними сегментами URL через Route Params, витягування значень через useParams, необов'язкові сегменти, wildcard-маршрути (splat), типізація параметрів у TypeScript.
Обробка помилок та 404: ErrorBoundary і fallback-маршрути
Створення сторінок 404 для неіснуючих маршрутів, обробка помилок рендерингу через ErrorBoundary, catch-all маршрути через wildcard, типізовані компоненти помилок, користувацький UX для помилкових станів.