В крупномасштабных веб-приложениях большинство адресов страниц невозможно зафиксировать статически на этапе написания кода. Каталоги товаров, блоги, документация и профили пользователей оперируют сотнями тысяч сущностей. Создание физических файлов для каждого 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 16 и 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. Однако если разработчик случайно поместит динамическую логику обработки нового поста в [id], возникнет непредсказуемое поведение.
Как избежать: Проектируйте статические служебные пути (new, edit, settings) в явных директориях и избегайте совпадения динамических id с зарезервированными ключевыми словами.
Установка флага export const dynamicParams = false; предписывает серверу возвращать 404 для любого URL, который не был явно возвращен функцией generateStaticParams(). Если в базе данных появляется новая запись после сборки проекта, пользователи не смогут ее открыть до следующего деплоя.
Как избежать: Оставляйте dynamicParams = true (поведение по умолчанию), если требуется генерация новых страниц на лету через Incremental Static Regeneration (ISR).