React Router v7: Маршрутизація в SPA

Навігація та корисні хуки React Router

Програмна навігація через useNavigate, активні посилання через NavLink, робота з пошуковими параметрами через useSearchParams, відстеження локації через useLocation, інтеграція з історією браузера.

Навігація та корисні хуки 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 — це спеціалізована версія 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: унікальний ключ локації (змінюється при навігації).

Практичне завдання


Підсумок

✅ Що ми опанували

  • Програмну навігацію через useNavigate() без взаємодії користувача.
  • Активні посилання через <NavLink> з автоматичним класом .active.
  • Роботу з пошуковими параметрами через useSearchParams.
  • Відстеження поточної локації через useLocation.
  • Передачу стану через navigate(path, { state }) та location.state.

🎯 Ключові практики

  • useNavigate для програмної навігації, Link/NavLink для взаємодії користувача.
  • replace: true для редиректів, де користувач не має повертатись назад.
  • useSearchParams для стану, що має бути шарабельним та букмаркабельним.
  • location.state для тимчасового контексту, що не має відображатись в URL.
Copyright © 2026