У великомасштабних вебзастосунках більшість адрес сторінок неможливо зафіксувати статично на етапі написання коду. Каталоги товарів, блоги, документація та профілі користувачів оперують сотнями тисяч сутностей. Створення фізичних файлів для кожної окремої URL-адреси є нераціональним і технічно неприйнятним.
Next.js App Router вирішує це завдання через параметризовані сегменти файлової системи — динамічні маршрути (Dynamic Routes). Замість жорстко заданого імені каталогу розробник оголошує параметризований шаблон у квадратних дужках. Збирач Turbopack компілює таку структуру в динамічний вузол префіксного дерева маршрутизатора (Radix Tree), витягуючи значення з URL і безпечно передаючи їх у компонент сторінки, макет або функцію generateMetadata.
В App Router динамічний сегмент (Dynamic Segment) створюється шляхом взяття імені папки у квадратні дужки. Наприклад, папка app/products/[id] перехоплює запити на кшталт /products/101 або /products/macbook-pro, зіставляючи значення сегмента з ключем id.
[param]): Захоплює строго один сегмент URL між двома слешами (/blog/nextjs).[...param]): Перехоплює довільну кількість вкладених сегментів у вигляді масиву рядків. Запит до базового шляху /docs без параметрів поверне 404.[[...param]]): Перехоплює як вкладені шляхи, так і сам базовий маршрут (де параметр дорівнює undefined).Коли одна й та сама вхідна адреса підходить під кілька файлових структур, роутер Turbopack розв'язує колізію згідно зі суворим правилом пріоритету сегментів (Route Precedence): Статичні маршрути > Динамічні сегменти [id] > Catch-all [...slug] > Optional Catch-all [[...slug]].
Turbopack парсить вхідний HTTP URI на токени за розділювачем слеш: /docs/api/v1 -> ['docs', 'api', 'v1'].
Відбувається пошук за префіксним деревом із перевіркою статичних маршрутів, поодиноких [param] та жадібних catch-all [...slug].
Значення сегментів упаковуються в Promise<params> для забезпечення незалежного стрімінгу статичної оболонки (PPR).
Сервер запускає паралельну компіляцію page.tsx та generateMetadata. Виклики await params розв'язують доступ до змінних шляху.
RSC Flight Payload передається в браузер. Клієнтські компоненти розпаковують параметри через хук use(paramsPromise).
Чому в Next.js 15+ і React 19 властивість params у серверному компоненті page.tsx оголошена як Promise<{ slug: string }>, а не як синхронний об'єкт?
import { Metadata } from 'next';
import { notFound } from 'next/navigation';
interface BlogPostPageProps {
params: Promise<{ slug: string }>;
searchParams: Promise<{ ref?: string }>;
}
export async function generateMetadata(props: BlogPostPageProps): Promise<Metadata> {
const { slug } = await props.params;
return {
title: `Стаття: ${slug}`,
description: `Детальний розбір архітектури ${slug}`,
};
}
export default async function BlogPostPage(props: BlogPostPageProps) {
const { slug } = await props.params;
const { ref } = await props.searchParams;
if (slug === 'forbidden-topic') {
notFound();
}
return (
<article className="p-6">
<h1 className="text-2xl font-bold">Публікація: {slug}</h1>
{ref && <p className="text-xs text-text-muted">Джерело переходу: {ref}</p>}
</article>
);
}Якщо динамічний параметр необхідно прочитати безпосередньо в клієнтському компоненті без передавання примітивного значення через props:
'use client';
import { use } from 'react';
interface SlugBadgeProps {
paramsPromise: Promise<{ slug: string }>;
}
export function SlugBadge({ paramsPromise }: SlugBadgeProps) {
// Хук use() розгортає Promise безпосередньо в тілі клієнтського компонента
const { slug } = use(paramsPromise);
return (
<span className="px-2 py-1 rounded bg-accent-violet/10 text-accent-violet border border-accent-violet/20 font-mono text-xs">
Поточний slug: {slug}
</span>
);
}Функція generateStaticParams() дозволяє заздалегідь згенерувати статичні HTML-сторінки для динамічних маршрутів на етапі збірки (next build), забезпечуючи швидкість рівня SSG:
// 1. Визначення списку шляхів для пререндерингу на етапі збірки
export async function generateStaticParams() {
const products = [
{ id: 'prod-1' },
{ id: 'prod-2' },
{ id: 'prod-3' },
];
return products.map((product) => ({
id: product.id,
}));
}
// 2. Керування поведінкою для невідомих параметрів
// true: генерувати сторінку на льоту при першому запиті (On-demand ISR)
// false: негайно віддавати 404 для будь-яких параметрів поза generateStaticParams
export const dynamicParams = true;
interface ProductPageProps {
params: Promise<{ id: string }>;
}
export default async function ProductPage({ params }: ProductPageProps) {
const { id } = await params;
return <div>Товар: {id}</div>;
}Що станеться, якщо в generateStaticParams() і в тілі компонента ProductPage викликається одна й та сама функція fetch('https://api.example.com/products')?
interface DocsPageProps {
params: Promise<{ slug: string[] }>;
}
export async function generateStaticParams() {
return [
{ slug: ['getting-started'] },
{ slug: ['architecture', 'routing'] },
{ slug: ['architecture', 'rendering', 'rsc'] },
];
}
export default async function DocsPage({ params }: DocsPageProps) {
const { slug } = await params;
const currentPath = slug.join(' / ');
return (
<div className="space-y-3 p-6">
<div className="text-xs text-text-muted font-mono">
Хлібні крихти: docs / {currentPath}
</div>
<h1 className="text-2xl font-bold">
Розділ: {slug[slug.length - 1]}
</h1>
<p className="text-sm text-text-secondary">
Глибина вкладеності сегмента: {slug.length}
</p>
</div>
);
}Сегмент app/shop/[[...slug]]/page.tsx обробляє як каталог верхнього рівня /shop, так і вкладені категорії /shop/shoes/nike:
interface ShopPageProps {
params: Promise<{ slug?: string[] }>;
}
export default async function ShopPage({ params }: ShopPageProps) {
const { slug } = await params;
// Базовий маршрут /shop (slug дорівнює undefined)
if (!slug || slug.length === 0) {
return <h1>Головна вітрина магазину (Усі категорії)</h1>;
}
// Вкладений маршрут /shop/category/subcategory
return (
<div>
<h2>Фільтр товарів: {slug.join(' > ')}</h2>
</div>
);
}Звернення до props.params.slug без await або хука use() призводить до попередження рантайму та помилок у production-збірці:
Error: Route "[slug]" used params.slug. params should be awaited before using its properties.
Як уникнути: Завжди використовуйте деструктуризацію після виклику: const { slug } = await props.params;.
Якщо в проєкті одночасно існують папки app/posts/new/page.tsx та app/posts/[id]/page.tsx, запит до /posts/new буде коректно перехоплено статичним файлом завдяки правилу Route Precedence. Проте якщо розробник помилково очікує динамічну логіку для new, виникне неочікувана поведінка.
Як уникнути: Проєктуйте статичні службові шляхи (new, edit, settings) у явних каталогах та уникайте збігу динамічних id із зарезервованими ключовими словами.
Встановлення прапорця export const dynamicParams = false; наказує серверу повертати 404 для будь-якого URL, який не був явно повернений функцією generateStaticParams(). Якщо в базі даних з'являється новий запис після збірки проєкту, користувачі не зможуть його відкрити до наступного деплою.
Як уникнути: Залишайте dynamicParams = true (поведінка за замовчуванням), якщо потрібна генерація нових сторінок на льоту через Incremental Static Regeneration (ISR).