nmk

Лекція №12 (2 години). Маршрутизація у SPA: Архітектура React Router

План лекції

  1. Концепція Client-Side Routing (Клієнтська маршрутизація) порівняно зі Server-Side Routing.
  2. API браузера window.history як фундамент маршрутизації в Single Page Applications.
  3. Еволюція React Router: від v5 до парадигми v6 (Data Routers).
  4. Базове налаштування: BrowserRouter, компонент Routes та механізм співпадіння шляхів (Route Matching).
  5. Динамічна маршрутизація: URL-параметри (useParams) та Query-рядки (useSearchParams).
  6. Вкладені маршрути (Nested Routes) та патерн компонування макетів через <Outlet />.
  7. Програмна навігація: хук useNavigate, переадресація (<Navigate />) та управління стеком історії браузера.

Перелік умовних скорочень

Вступ

Концепція клієнтської маршрутизації. Налаштування BrowserRouter, Routes та Route. Вивчення динамічних параметрів шляху (useParams), вкладених маршрутів (nested routes) та програмної навігації (useNavigate).

В епоху класичних веб-сайтів (MPA - Multi-Page Applications) навігація між сторінками означала надсилання нового HTTP-запиту до сервера, отримання нового HTML-документа і повне “блимання” екрану при його перемальовуванні браузером. Сучасні React-додатки будуються за парадигмою SPA (Single Page Application). Сервер віддає index.html лише один раз (при першому заході користувача). Всі подальші “переходи” між сторінками — це ілюзія. JavaScript просто перехоплює клік по посиланню, змінює URL-адресу в стрічці браузера і підміняє один React-компонент на інший прямо в пам’яті. Бібліотека React Router є світовим стандартом (De Factor Standard) для керування цією ілюзією, перетворюючи набір розрізнених компонентів на повноцінний програмний продукт із власною архітектурою шляхів.


1. Концепція Client-Side Routing проти Server-Side Routing

Щоб зрозуміти цінність React Router, порівняємо дві парадигми:

Server-Side Routing (Традиційний PHP, Python Django):

  1. Користувач клікає <a href="/about">.
  2. Браузер “вбиває” поточну сторінку і відправляє GET /about запит на сервер.
  3. Сервер обчислює HTML сторінки About і повертає його.
  4. Браузер рендерить нову сторінку з нуля. Зв’язок розірвано, локальний JavaScript стан (useState) втрачено назавжди.

Client-Side Routing (React + React Router):

  1. Користувач клікає <Link to="/about">.
  2. React Router перехоплює подію кліку (через e.preventDefault()). ЗАПИТ НА СЕРВЕР НЕ ЙДЕ!
  3. React Router змінює URL в адресному рядку браузера за допомогою window.history.pushState().
  4. Дерево компонентів миттєво реагує на новий URL: старий компонент екрану “Home” розмонтовується (Unmount), новий компонент “About” монтується. Глобальний стан (Context, Redux) зберігається недоторканим.

2. API браузера window.history як фундамент

Жодна клієнтська бібліотека маршрутизації не працювала б без нативного HTML5 History API. До появи HTML5 розробникам доводилося використовувати “хеш-маршрутизацію” (Hash Routing), де URL виглядав як site.com/#/about. Зміна всього, що йде після #, не викликає перезавантаження сторінки в браузері.

З появою History API браузери надали JavaScript-методи для реальної маніпуляції URL:

React Router інкапсулює цю логіку, надаючи декларативний інтерфейс для розробників.


3. Еволюція React Router: від v5 до парадигми v6

Бібліотека React Router пережила кілька радикальних архітектурних змін. Для розробника 4-го курсу важливо знати, що версія v6 принесла зміну парадигми.

Принципові відмінності v6 від старих версій:

  1. Відмова від компонента <Switch> на користь <Routes>.
  2. Алгоритм співпадіння шляхів став “розумним” (Smart Route Matching). Раніше порядок реєстрації маршрутів мав значення (вимагався атрибут exact). У v6 алгоритм сам знаходить найбільш специфічний (точний) маршрут, незалежно від того, як ви відсортували компоненти в коді.
  3. Поява Data Routers (в v6.4+), яка інтегрувала процеси завантаження даних (фетчінгу) безпосередньо у визначення маршрутів через концепцію Loader’ів, наближаючи архітектуру React Router до фреймворків типу Remix чи Next.js.

4. Базове налаштування: Співпадіння шляхів (Route Matching)

Будь-який маршрутизатор потребує Провайдера, який відстежує стан URL.

import { BrowserRouter, Routes, Route, Link } from "react-router-dom";

const App = () => {
  return (
    // 1. Огортаємо РУТ додатка в провайдер. Він парсить поточний URL.
    <BrowserRouter>
      {/* 2. Навігація замість <a> тегів */}
      <nav>
        <Link to="/">Головна</Link>
        <Link to="/about">Про нас</Link>
      </nav>

      {/* 3. Зона, де буде мінятися контент */}
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
        {/* Wildcard (Зірочка) для сторінки помилки (404 Not Found) */}
        <Route path="*" element={<NotFound />} />
      </Routes>
    </BrowserRouter>
  );
};

Важливо: Використання звичайного <a href="/about"> “ламає” SPA, оскільки браузер виконає жорстке перезавантаження. Компонент <Link> гарантує, що перехід відбудеться безшовно (без втрати стану useState).


5. Динамічна маршрутизація: параметри та Query-рядки

Інженерним стандартом є створення сторінок, які можуть приймати параметри (наприклад, відображення профілю конкретного користувача: /users/123, /users/alex).

Динамічні сегменти (Path Parameters)

Визначаються двокрапкою : у path.

// 1. Визначення маршруту
<Route path="/users/:userId" element={<UserProfile />} />;

// 2. Отримання в компоненті через хук useParams
import { useParams } from "react-router-dom";

const UserProfile = () => {
  // З URL "/users/123" хук витягне { userId: '123' }
  const { userId } = useParams();

  useEffect(() => {
    fetchUserData(userId); // Фетчимо дані для поточного ID
  }, [userId]);

  return <div>Профіль користувача №{userId}</div>;
};

Рядок запиту (Query/Search Parameters)

Це частина URL після знаку запитання, яка використовується для фільтрації, пагінації чи пошуку (напр., /products?category=laptops&sort=price). Вони не визначаються в конфігурації <Route>, а просто зчитуються “на льоту”.

import { useSearchParams } from "react-router-dom";

const ProductList = () => {
  // useSearchParams працює аналогічно до useState (гетер і функція сеттер)
  const [searchParams, setSearchParams] = useSearchParams();

  const category = searchParams.get("category"); // "laptops"

  const handleSort = () => {
    // Збереже поточні параметри і змінить/додасть новий
    setSearchParams({ category, sort: "price" });
  };
};

Перевага useSearchParams полягає в тому, що стан фільтрів чи пагінації зберігається безпосередньо в URL. Якщо користувач скопіює посилання і відправить другу, друг побачить точно той самий відфільтрований список. Це називається “Deep Linking”. Знаходження фільтрів у локальному useState не дало б такої можливості.


6. Вкладені маршрути (Nested Routes) та макети (<Outlet />)

В ентерпрайз-додатках сторінки часто мають спільний “каркас” (Layout) — наприклад, панель адміністратора з єдиним лівим меню та верхнім хедером. У React Router v6 для цього використовується унікальний компонент вікна прокидання — <Outlet />.

// 1. Структура макету
const DashboardLayout = () => {
  return (
    <div className="layout">
      <Sidebar />
      <main className="content">
        {/* Сюди "проваляться" вкладені (дочірні) сторінки */}
        <Outlet />
      </main>
    </div>
  );
};

// 2. Налаштування роутингу
<Routes>
  {/* Батьківський маршрут. Має префікс /admin */}
  <Route path="/admin" element={<DashboardLayout />}>
    {/* Дочірні маршрути автоматично успадковують префікс /admin */}
    <Route index element={<DashboardHome />} /> {/* URL: /admin */}
    <Route path="users" element={<AdminUsers />} /> {/* URL: /admin/users */}
    <Route path="settings" element={<AdminSettings />} />{" "}
    {/* URL: /admin/settings */}
  </Route>
</Routes>;

Якщо ви введете URL /admin/users, React Router відрендерить компонент <DashboardLayout>, а замість його внутрішнього <Outlet /> відрендерить компонент <AdminUsers>. Це радикально скорочує дублювання коду обгорток.


7. Програмна навігація: хук useNavigate

Іноді перехід між сторінками має відбуватися не в результаті кліку по посиланню (Link), а як наслідок певної дії бізнес-логіки (успішний логін, завершення оплати в кошику, таймаут сесії). Це називається Програмна (Імперативна) навігація.

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

const LoginForm = () => {
  const navigate = useNavigate();

  const handleLogin = async () => {
    const isSuccess = await authenticateUser();
    if (isSuccess) {
      // Імперативний перехід на Dashboard
      navigate("/dashboard", {
        replace: true, // Опція replace видаляє сторінку логіну з історії браузера
      });
    }
  };

  return <button onClick={handleLogin}>Увійти</button>;
};

Опція { replace: true } (Replace State) гарантує, що якщо користувач перейде на Dashboard і натисне кнопку “Назад” у браузері, його не поверне назад на сторінку логіну (де він щойно успішно авторизувався), що є правильним UX-патерном для потоків автентифікації.

Крім хука useNavigate, існує декларативний компонент-аналог <Navigate />, який найчастіше використовується для написання логіки захищених маршрутів (Protected Routes):

const ProtectedRoute = ({ children }) => {
  const { user } = useAuth();

  if (!user) {
    // Якщо користувач не залогінений - миттєво редиректимо його на /login
    return <Navigate to="/login" replace />;
  }

  return children;
};

Висновки

  1. Перехід на парадигму Single Page Application (SPA) за допомогою Client-Side Routing дозволяє уникнути затратного перемальовування всієї сторінки, зберігаючи локальний глобальний стан додатку (Context/Redux) між “переходами”.
  2. React Router абстрагує роботу з нативним API браузера (window.history), надаючи розробнику декларативний інтерфейс для побудови архітектури шляхів.
  3. Версія React Router v6 значно покращила алгоритм співпадіння маршрутів та формалізувала концепцію <Routes>, зробивши використання exact-прапорців непотрібним.
  4. Вкладені маршрути (Nested Routes) разом з компонентом <Outlet> дозволяють легко і без дублювання створювати багаторівневі макети (Layouts), наприклад, розділяючи публічну частину додатку та захищену панель адміністратора.
  5. Глибоке розуміння різниці між URL параметрами (useParams - ідентифікація ресурсів) та Search Query (useSearchParams - налаштування переглядів, фільтрація) є необхідним для побудови правильної архітектури Deep-лінків у додатку.
  6. Програмна навігація через useNavigate з розумінням дії параметра replace дозволяє правильно управляти стеком (історією) візитів браузера, запобігаючи логічним пасткам (наприклад, повернення на форму оплати після її успішного завершення натисканням клавіші “Назад”).

Джерела

  1. Офіційна документація React Router. URL: https://reactrouter.com/
  2. React Router: “Upgrading from v5”. URL: https://reactrouter.com/en/main/upgrading/v5
  3. MDN Web Docs. “History API”. URL: https://developer.mozilla.org/en-US/docs/Web/API/History_API
  4. Kent C. Dodds. “How I structure React applications” (Розділ: Routing & Data fetching).
  5. Remix Documentation (для порівняння архітектури Data-Driven Routers). URL: https://remix.run/
  6. UIDotDev Blog. “React Router v6 in Depth”. URL: https://ui.dev/react-router-v6
  7. “SPA vs MPA (Single Page Application vs Multi-Page Application)”. AWS Serverless Documentation. URL: https://aws.amazon.com/compare/the-difference-between-spa-and-mpa/

Запитання для самоперевірки

  1. З технічної точки зору взаємодії клієнта з сервером, яка кардинальна різниця відбувається, якщо користувач клікне на тег <a href="/about"> порівняно з компонентом <Link to="/about"> у React Router? Що станеться з об’єктом стану Redux/Zustand у кожному з двох випадків?
  2. В чому полягала архітектурна зміна механізму парсингу маршрутів у React Router v6 (відмова від <Switch>)? Чому порядок визначення маршрутів більше не має критичного значення?
  3. В якій ситуації і з якою метою розробник повинен використати компонент <Outlet />? Опишіть патерн “Вкладені маршрути” (Nested Routes) на прикладі панелі управління інтернет-магазину.
  4. Порівняйте призначення та застосування хуків useParams та useSearchParams. Який із цих хуків доцільно використати для сторінки детального опису товару, а який — для сортування цього товару за ціною?
  5. Чому при програмному редіректі неавторизованого користувача на сторінку /login (в системі захищених рутів) інженери обов’язково додають опцію { replace: true } у метод navigate()? Як це впливає на апаратні кнопки браузера?