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

Динамічні маршрути та параметри URL

Робота з динамічними сегментами URL через Route Params, витягування значень через useParams, необов'язкові сегменти, wildcard-маршрути (splat), типізація параметрів у TypeScript.

Динамічні маршрути та параметри URL

🔗 Офіційний референс: React Router v7 - URL Values (Route Params)

🎯 Мета лекції

  • Опанувати створення динамічних маршрутів із змінними сегментами URL.
  • Навчитися витягувати значення параметрів через хук useParams.
  • Зрозуміти різницю між обов'язковими та необов'язковими сегментами.
  • Застосувати wildcard-маршрути для обробки вкладених шляхів.
  • Забезпечити типобезпеку параметрів через TypeScript.

🔑 Ключові терміни

  • Route Params (Параметри маршруту): змінні частини URL, позначені префіксом : у шляху маршруту.
  • useParams: хук React Router, що повертає об'єкт із розпарсеними значеннями динамічних сегментів.
  • Optional Segment (Необов'язковий сегмент): сегмент URL, позначений суфіксом ?, який може бути відсутнім.
  • Splat / Wildcard (*): спеціальний сегмент, що збігається з будь-якою кількістю вкладених сегментів.

Проблема статичних маршрутів

У попередніх лекціях ми визначали маршрути зі статичними шляхами:

<Route path="/users" element={<UsersList />} />
<Route path="/products" element={<ProductsList />} />

Але що, якщо потрібно показати конкретного користувача чи конкретний продукт? Неможливо створити окремий маршрут для кожного ID:

// ❌ Це не масштабується!
<Route path="/users/1" element={<UserProfile userId="1" />} />
<Route path="/users/2" element={<UserProfile userId="2" />} />
<Route path="/users/3" element={<UserProfile userId="3" />} />
// ... тисячі маршрутів
Динамічні маршрути вирішують цю проблему: замість жорстко закодованих значень ми визначаємо змінний сегмент у шляху маршруту, а React Router автоматично парсить його значення з URL та надає компоненту.

Базовий синтаксис: динамічний сегмент через :

Динамічний сегмент позначається префіксом двокрапки ::

<Route path="/users/:userId" element={<UserProfile />} />

Цей маршрут збігається з будь-яким URL вигляду:

  • /users/1 → userId = "1"
  • /users/abc-123 → userId = "abc-123"
  • /users/john-doe → userId = "john-doe"

Але не збігається з:

  • /users (відсутній сегмент)
  • /users/123/edit (зайвий сегмент)

Витягування параметрів через useParams

Всередині компонента значення динамічних сегментів доступні через хук useParams():

import { useParams } from "react-router";

function UserProfile() {
  const { userId } = useParams();

  return (
    <div>
      <h1>Профіль користувача</h1>
      <p>ID користувача: {userId}</p>
      <p>Завантаження даних для користувача {userId}...</p>
    </div>
  );
}
useParams() повертає об'єкт, ключі якого відповідають іменам динамічних сегментів у шляху маршруту. Значення завжди рядки (string), навіть якщо у URL число.
const params = useParams();
// params = { userId: "123" }

const { userId } = useParams();
// userId: string | undefined

Типізація параметрів у TypeScript

useParams за замовчуванням має дуже широкий тип: Readonly<Params<string>>, де всі ключі необов'язкові (string | undefined). Для строгої типобезпеки можна явно вказати структуру параметрів:

import { useParams } from "react-router";

// Визначаємо тип параметрів
type UserProfileParams = {
  userId: string;
};

function UserProfile() {
  const { userId } = useParams<UserProfileParams>();
  // userId: string | undefined (все ще може бути undefined!)

  if (!userId) {
    return <div>Помилка: ID користувача не вказано</div>;
  }

  // Тепер TypeScript знає, що userId точно існує
  return (
    <div>
      <h1>Профіль користувача {userId}</h1>
    </div>
  );
}
Важливо: навіть із типізацією useParams<T>(), TypeScript не гарантує, що параметр присутній у runtime — користувач може вручну ввести невалідний URL. Завжди перевіряйте наявність критичних параметрів через if (!param).

Кілька динамічних сегментів в одному маршруті

Маршрут може містити кілька динамічних сегментів:

<Route path="/categories/:categoryId/products/:productId" element={<ProductDetails />} />

URL /categories/electronics/products/laptop-x1 парситься як:

{ categoryId: "electronics", productId: "laptop-x1" }
import { useParams } from "react-router";

type ProductDetailsParams = {
  categoryId: string;
  productId: string;
};

function ProductDetails() {
  const { categoryId, productId } = useParams<ProductDetailsParams>();

  if (!categoryId || !productId) {
    return <div>Помилка: Не вказано категорію або продукт</div>;
  }

  return (
    <div>
      <h1>Продукт: {productId}</h1>
      <p>Категорія: {categoryId}</p>
    </div>
  );
}
Імена динамічних сегментів мають бути унікальними у межах одного маршруту. Інакше останнє значення перезапише попереднє:
// ❌ НЕПРАВИЛЬНО: обидва сегменти називаються "id"
<Route path="/users/:id/posts/:id" element={<Post />} />
// params = { id: "456" } — значення з другого сегмента перезаписало перше!

// ✅ ПРАВИЛЬНО: унікальні імена
<Route path="/users/:userId/posts/:postId" element={<Post />} />
// params = { userId: "123", postId: "456" }

Необов'язкові сегменти через ?

Іноді сегмент URL може бути присутнім або відсутнім. Необов'язковий сегмент позначається суфіксом ?:

<Route path="/users/:userId/edit?" element={<UserPage />} />

Цей маршрут збігається з обома URL:

  • /users/123 → { userId: "123" }
  • /users/123/edit → { userId: "123" }
import { useParams, useLocation } from "react-router";

function UserPage() {
  const { userId } = useParams();
  const location = useLocation();

  const isEditMode = location.pathname.endsWith("/edit");

  return (
    <div>
      <h1>{isEditMode ? "Редагування" : "Перегляд"} профілю {userId}</h1>
      {isEditMode ? <EditForm /> : <ProfileView />}
    </div>
  );
}
Альтернативний підхід: замість необов'язкового сегмента можна використовувати вкладені маршрути для кращої семантики:
<Route path="/users/:userId">
  <Route index element={<UserView />} />
  <Route path="edit" element={<UserEdit />} />
</Route>
Це чіткіше відділяє режим перегляду від режиму редагування.

Необов'язкові статичні сегменти

Не тільки динамічні сегменти можуть бути необов'язковими — статичні теж:

<Route path="/:lang?/categories" element={<Categories />} />

Цей маршрут збігається з:

  • /categories → { lang: undefined }
  • /en/categories → { lang: "en" }
  • /uk/categories → { lang: "uk" }
function Categories() {
  const { lang } = useParams();
  const language = lang || "uk"; // За замовчуванням українська

  return (
    <div>
      <h1>Категорії (мова: {language})</h1>
    </div>
  );
}

Wildcard-маршрути: збіг вкладених шляхів через *

Спеціальний сегмент * (splat, wildcard) збігається з будь-якою кількістю вкладених сегментів, включаючи /:

<Route path="/docs/*" element={<Documentation />} />

Цей маршрут збігається з:

  • /docs/getting-started
  • /docs/api/authentication
  • /docs/tutorials/react/hooks/use-effect

Значення після /docs/ доступне через ключ "*":

import { useParams } from "react-router";

function Documentation() {
  const params = useParams();
  const docPath = params["*"]; // "getting-started" або "api/authentication"

  // Альтернативно: деструктуризація з перейменуванням
  const { "*": splat } = useParams();

  return (
    <div>
      <h1>Документація</h1>
      <p>Шлях: {docPath}</p>
      <p>Завантаження вмісту для {splat}...</p>
    </div>
  );
}
Типовий Use Case для wildcard-маршрутів:
  • Документація з вкладеною структурою файлів (/docs/api/users/create).
  • Файловий менеджер (/files/projects/2024/reports/january.pdf).
  • Маршрутизація Markdown-файлів у статичних сайтах.
Wildcard дозволяє React Router делегувати обробку вкладених шляхів вашому компоненту — ви самі визначаєте, як інтерпретувати splat.

Пріоритет збігу маршрутів

Коли кілька маршрутів потенційно збігаються з URL, React Router обирає найбільш специфічний маршрут:

<Routes>
  <Route path="/users/new" element={<CreateUser />} />
  <Route path="/users/:userId" element={<UserProfile />} />
</Routes>

URL /users/new збігається з обома маршрутами, але React Router обере перший (статичний сегмент new має пріоритет над динамічним :userId).

Порядок визначення маршрутів має значення лише коли маршрути мають однаковий рівень специфічності. У прикладі вище порядок не важливий, оскільки статичний маршрут завжди має пріоритет.Проте, якщо обидва маршрути динамічні, порядок визначає, який спрацює першим:
<Routes>
  <Route path="/users/:userId" element={<UserProfile />} />
  <Route path="/users/:username" element={<UserByUsername />} />
</Routes>
Обидва маршрути ідентичні за специфічністю — спрацює перший. Це приклад поганого дизайну маршрутів — краще розрізняти їх через префікси:
<Routes>
  <Route path="/users/id/:userId" element={<UserProfile />} />
  <Route path="/users/username/:username" element={<UserByUsername />} />
</Routes>

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


Підсумок

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

  • Створення динамічних маршрутів через синтаксис :paramName.
  • Витягування значень параметрів через хук useParams().
  • Типізацію параметрів через useParams<T>() для TypeScript.
  • Необов'язкові сегменти через суфікс ?.
  • Wildcard-маршрути через * для збігу вкладених шляхів.
  • Пріоритетність збігу маршрутів (статичні мають перевагу над динамічними).

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

  • Завжди валідуйте параметри: перевіряйте наявність критичних параметрів через if (!param).
  • Використовуйте типізацію: useParams<T>() покращує автодоповнення та перевірку типів.
  • Уникайте дублювання імен: кожен динамічний сегмент у маршруті має бути унікальним.
  • Wildcard для файлових структур: делегуйте обробку вкладених шляхів компоненту через *.
Copyright © 2026