Динамічні маршрути та параметри URL
Динамічні маршрути та параметри 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" />} />
// ... тисячі маршрутів
Базовий синтаксис: динамічний сегмент через :
Динамічний сегмент позначається префіксом двокрапки ::
<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>
);
}
- Документація з вкладеною структурою файлів (
/docs/api/users/create). - Файловий менеджер (
/files/projects/2024/reports/january.pdf). - Маршрутизація Markdown-файлів у статичних сайтах.
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>
Практичне завдання
Створіть маршрути для інтернет-магазину:
/products— список усіх продуктів./products/:productId— деталі конкретного продукту./categories/:categorySlug— список продуктів у категорії./categories/:categorySlug/products/:productId— деталі продукту всередині категорії.
Вимоги:
- Використайте
useParamsдля витягуванняproductIdтаcategorySlug. - Виведіть хлібні крихти (breadcrumbs) на сторінці продукту всередині категорії.
- Додайте типізацію TypeScript для параметрів.
import { Routes, Route, Link, useParams } from "react-router";
// Типи параметрів
type ProductParams = {
productId: string;
};
type CategoryParams = {
categorySlug: string;
};
type CategoryProductParams = {
categorySlug: string;
productId: string;
};
// Список усіх продуктів
function ProductsList() {
return (
<div>
<h1>Усі продукти</h1>
<ul>
<li><Link to="/products/laptop-x1">Ноутбук X1</Link></li>
<li><Link to="/products/phone-pro">Телефон Pro</Link></li>
<li><Link to="/products/tablet-ultra">Планшет Ultra</Link></li>
</ul>
</div>
);
}
// Деталі продукту (без категорії)
function ProductDetails() {
const { productId } = useParams<ProductParams>();
if (!productId) {
return <div>Помилка: ID продукту не вказано</div>;
}
return (
<div>
<nav>
<Link to="/products">Усі продукти</Link> / {productId}
</nav>
<h1>Продукт: {productId}</h1>
<p>Опис, ціна, характеристики...</p>
</div>
);
}
// Список продуктів у категорії
function CategoryProducts() {
const { categorySlug } = useParams<CategoryParams>();
if (!categorySlug) {
return <div>Помилка: Категорія не вказана</div>;
}
return (
<div>
<h1>Категорія: {categorySlug}</h1>
<ul>
<li>
<Link to={`/categories/${categorySlug}/products/item-1`}>
Товар 1
</Link>
</li>
<li>
<Link to={`/categories/${categorySlug}/products/item-2`}>
Товар 2
</Link>
</li>
</ul>
</div>
);
}
// Деталі продукту всередині категорії
function CategoryProductDetails() {
const { categorySlug, productId } = useParams<CategoryProductParams>();
if (!categorySlug || !productId) {
return <div>Помилка: Не вказано категорію або продукт</div>;
}
return (
<div>
<nav style={{ marginBottom: "1rem" }}>
<Link to="/">Головна</Link>
{" / "}
<Link to={`/categories/${categorySlug}`}>{categorySlug}</Link>
{" / "}
{productId}
</nav>
<h1>Продукт: {productId}</h1>
<p>Категорія: {categorySlug}</p>
<p>Детальний опис продукту з урахуванням категорії...</p>
</div>
);
}
function App() {
return (
<Routes>
<Route path="/" element={<h1>Головна сторінка магазину</h1>} />
{/* Продукти без категорії */}
<Route path="/products">
<Route index element={<ProductsList />} />
<Route path=":productId" element={<ProductDetails />} />
</Route>
{/* Категорії та продукти всередині категорій */}
<Route path="/categories/:categorySlug">
<Route index element={<CategoryProducts />} />
<Route path="products/:productId" element={<CategoryProductDetails />} />
</Route>
</Routes>
);
}
export default App;
Пояснення:
- Breadcrumbs (хлібні крихти): у
CategoryProductDetailsвикористовуємо обидва параметри для побудови навігаційного шляху. - Типізація: кожен компонент явно вказує тип параметрів через дженерик
useParams<T>(). - Вкладені маршрути:
/productsта/categories/:categorySlugмають власні index та дочірні маршрути.
URL-шляхи:
/products→ ProductsList/products/laptop-x1→ ProductDetails/categories/electronics→ CategoryProducts/categories/electronics/products/laptop-x1→ CategoryProductDetails (з breadcrumbs)
Підсумок
✅ Що ми опанували
- Створення динамічних маршрутів через синтаксис
:paramName. - Витягування значень параметрів через хук
useParams(). - Типізацію параметрів через
useParams<T>()для TypeScript. - Необов'язкові сегменти через суфікс
?. - Wildcard-маршрути через
*для збігу вкладених шляхів. - Пріоритетність збігу маршрутів (статичні мають перевагу над динамічними).
🎯 Ключові практики
- Завжди валідуйте параметри: перевіряйте наявність критичних параметрів через
if (!param). - Використовуйте типізацію:
useParams<T>()покращує автодоповнення та перевірку типів. - Уникайте дублювання імен: кожен динамічний сегмент у маршруті має бути унікальним.
- Wildcard для файлових структур: делегуйте обробку вкладених шляхів компоненту через
*.
Вкладені маршрути та Outlet: архітектура складних макетів
Організація вкладеної маршрутизації через Route-дерево, використання Outlet для рендерингу дочірніх компонентів, Layout Routes для спільних обгорток, Index Routes як сторінки за замовчуванням.
Навігація та корисні хуки React Router
Програмна навігація через useNavigate, активні посилання через NavLink, робота з пошуковими параметрами через useSearchParams, відстеження локації через useLocation, інтеграція з історією браузера.