Skip to main content

Маршрутизація в Next.js (маршрутизація на основі файлів)

Маршрутизація на основі файлів (file-based routing) у Next.js будує URL-адреси зі структури папок у директорії app. Кожен файл page.tsx визначає маршрут. Жодних конфігураційних файлів, жодних <Route> декларацій, жодних імпортів з react-router.

Теорія

TL;DR

  • Назва папки = сегмент URL; app/docs/page.tsx стає /docs
  • [slug] у назві папки захоплює той сегмент URL в params
  • [...slug] захоплює 1+ сегментів як масив; [[...slug]] захоплює 0+ (опціонально)
  • Групи маршрутів (name) організовують файли без зміни URL
  • Папка без page.tsx не створює маршрут, вона лише організовує код

Швидкий приклад

tsx
// app/page.tsx → URL: / export default function Home() { return <h1>Головна</h1> } // app/blog/page.tsx → URL: /blog export default function Blog() { return <h1>Блог</h1> } // app/blog/[slug]/page.tsx → URL: /blog/my-first-post export default function Post({ params }: { params: { slug: string } }) { return <h1>Пост: {params.slug}</h1> // "my-first-post" }

Next.js сканує app/ під час збірки і генерує manifest маршрутів зі структури папок. Цей manifest сервер використовує для зіставлення запитів.

Як це працює всередині

Next.js (через Turbopack або webpack) рекурсивно обходить директорію app під час збірки. Кожен page.tsx стає сегментом маршруту на основі шляху до папки. Динамічні сегменти, наприклад [slug], перетворюються на regex-патерни в manifest маршрутів (build-manifest.json). На момент запиту сервер зіставляє URL з цими патернами і витягує params.

Об'єкт params передається в компонент як prop через React Server Components, тому він доступний на сервері за замовчуванням. У клієнтських компонентах використовують useParams() з next/navigation. Це і є найчастіша помилка в код-рев'ю: серверний компонент переробляють на клієнтський і забувають оновити спосіб отримання params.

Динамічні маршрути

Квадратні дужки позначають папку як динамічний сегмент:

tsx
// app/shop/[category]/page.tsx → /shop/laptops, /shop/phones export default async function Category({ params }: { params: { category: string } }) { const products = await getProducts(params.category) return <h1>{params.category}</h1> }

Вкладеність працює так само. app/shop/[category]/[id]/page.tsx дає і params.category, і params.id за URL /shop/laptops/123.

Catch-all маршрути

[...slug] захоплює один або більше сегментів як масив:

tsx
// app/docs/[...slug]/page.tsx // /docs/react → slug = ['react'] // /docs/react/hooks → slug = ['react', 'hooks'] export default function Docs({ params }: { params: { slug: string[] } }) { return <div>{params.slug.join(' / ')}</div> }

[[...slug]] (подвійні дужки) також відповідає кореневому шляху. Тобто app/docs/[[...slug]]/page.tsx обробляє /docs (порожній масив) і будь-який вкладений шлях. Різниця одна: одинарний [...slug] вимагає хоча б одного сегмента, подвійний [[...slug]] робить усі сегменти опціональними. Саме це призводить до 404 при побудові індексної сторінки документації.

Групи маршрутів

Дужки створюють папку, яка групує маршрути без додавання до URL:

app/ (marketing)/ layout.tsx <- макет для маркетингу about/page.tsx -> /about pricing/page.tsx -> /pricing (platform)/ layout.tsx <- макет для платформи dashboard/page.tsx -> /dashboard

(marketing) і (platform) ніколи не з'являються в URL. Кожна група може мати власний layout.tsx, тому /about і /dashboard можуть використовувати повністю різні макети, живучи в одній директорії app/.

Паралельні маршрути

Паралельні маршрути дозволяють рендерити кілька незалежних секцій в одному макеті. Слоти визначаються через @:

app/dashboard/ layout.tsx page.tsx @stats/page.tsx @activity/page.tsx
tsx
// app/dashboard/layout.tsx export default function DashboardLayout({ children, stats, activity }: { children: React.ReactNode stats: React.ReactNode activity: React.ReactNode }) { return ( <div> {children} <div className="grid grid-cols-2 gap-4"> {stats} {activity} </div> </div> ) }

Кожен слот завантажується і стримується незалежно. @stats може показувати стан завантаження, поки @activity вже відрендерений.

Перехоплення маршрутів

Перечоплення маршрутів (intercepting routes) дозволяє показати маршрут в іншому контексті без зміни URL. Класичний приклад: клік на фото відкриває модальне вікно, але прямий перехід за URL /photos/123 показує повну сторінку.

(.) — той самий рівень (..) — один рівень вгору (..)(..) — два рівні вгору (...) — від кореня app

На IT Lead натискання на картку задачі відкриває модальне вікно з попереднім переглядом. Прямий доступ за тим самим URL показує повну сторінку задачі. Це і є перехоплення маршрутів на практиці.

Спеціальні файли

ФайлПризначення
page.tsxUI сторінки (робить сегмент доступним як маршрут)
layout.tsxСпільний макет, зберігається між навігаціями
loading.tsxSuspense-заглушка під час завантаження даних
error.tsxError boundary для сегмента
not-found.tsxКастомна 404 для сегмента
template.tsxЯк layout, але перемонтується при кожній навігації
route.tsAPI-ендпоінт (Route Handler, тільки App Router)

Папка без page.tsx не створює маршрут. Вона лише організовує код (колокацію). Next.js реєструє сегмент тільки там, де знаходить page.tsx.

Типові помилки

Помилка 1: Звернення до params напряму в клієнтському компоненті

tsx
// НЕПРАВИЛЬНО 'use client' export default function Page({ params }: { params: { slug: string } }) { return <div>{params.slug}</div> // undefined на клієнті } // ПРАВИЛЬНО 'use client' import { useParams } from 'next/navigation' export default function Page() { const params = useParams() // працює на клієнті return <div>{params.slug as string}</div> }

params як prop працює тільки в Server Components. Клієнтські компоненти потребують useParams().

Помилка 2: Використання [...slug] замість одиночного [id]

tsx
// app/[...userId]/page.tsx // /profile/123 → params.userId = ['profile', '123'] — захоплює занадто багато // Виправлення: app/profile/[userId]/page.tsx

Catch-all захоплює все в шляху, включаючи сегменти, які ти не планував захоплювати.

Помилка 3: Забути generateStaticParams для динамічних маршрутів

tsx
// Без цього /blog/[slug] рендериться на сервері при кожному запиті → повільний TTFB export async function generateStaticParams() { const posts = await getPosts() return posts.map((post) => ({ slug: post.slug })) }

Якщо slug-и відомі під час збірки, експортуй generateStaticParams. Next.js попередньо збере ці сторінки як статичний HTML.

Помилка 4: Плутати [...slug] і [[...slug]]

[...slug] на /docs без сегментів дасть 404. [[...slug]] на /docs збігається і повертає params.slug = []. Коли кореневий маршрут теж має збігатися, потрібні подвійні дужки.

Де зустрічається в реальних проектах

  • Блог Next.js: /blog/[slug] завантажує MDX-пости з файлової системи
  • Документація Vercel: /docs/[product]/[version]/api для мультипродуктової документації
  • IT Lead: перехоплення маршрутів для модального попереднього перегляду задач
  • Сайти на основі CMS: generateStaticParams попередньо збирає сторінки з API під час деплою
  • Застосунки типу Linear: [...id] catch-all для глибоких посилань на задачі

Follow-up питання

Q: Яка різниця між [...slug] і [[...slug]]?
A: [...slug] вимагає хоча б одного сегмента. Запит на /docs без подальшого шляху дасть 404. [[...slug]] робить усі сегменти опціональними, тому /docs збігається і повертає порожній масив.

Q: Що станеться на /docs/a/b, якщо файл app/docs/[slug]/page.tsx?
A: 404. Одинарний [slug] захоплює рівно один сегмент. Для багатосегментних шляхів потрібен [...slug].

Q: Чим відрізняються params і searchParams?
A: params береться зі шляху URL (/post/123 дає { id: '123' }). searchParams береться з рядка запиту (?draft=true дає { draft: 'true' }). Обидва доступні як props у Server Components.

Q: Як попередньо зібрати динамічні маршрути як статичні сторінки?
A: Експортуй generateStaticParams(), що повертає масив об'єктів з параметрами. Next.js запускає це під час збірки і генерує статичний HTML для кожного запису.

Q: (Senior) Middleware перехоплює /[slug] і перезаписує URL. Чи params залишаться в компоненті сторінки?
A: Так. Використовуй NextResponse.rewrite(new URL('/real-path', req.url)) у middleware. Перезапис змінює, який файл обробляє запит, але params витягуються з URL-патерну відповідного файлу і залишаються незмінними.

Приклади

Базова структура маршрутів

tsx
// app/page.tsx → / export default function Home() { return <h1>Головна</h1> } // app/problems/page.tsx → /problems export default function Problems() { return <h1>Задачі</h1> } // app/problems/[id]/page.tsx → /problems/fizzbuzz export default function Problem({ params }: { params: { id: string } }) { return <h1>Задача: {params.id}</h1> }

Назва папки стає сегментом URL. page.tsx робить його доступним. Це вся система.

Завантаження поста блогу з notFound

tsx
// app/blog/[slug]/page.tsx import { notFound } from 'next/navigation' async function getPost(slug: string) { const res = await fetch(`https://api.example.com/posts/${slug}`, { cache: 'force-cache' // кешується під час збірки або через ISR }) if (!res.ok) notFound() // активує найближчий not-found.tsx return res.json() } export default async function Post({ params }: { params: { slug: string } }) { const post = await getPost(params.slug) return ( <article> <h1>{post.title}</h1> <div dangerouslySetInnerHTML={{ __html: post.content }} /> </article> ) } // Попередньо збираємо відомі slug-и під час деплою export async function generateStaticParams() { const posts = await getPosts() return posts.map((p) => ({ slug: p.slug })) }

notFound() з next/navigation активує найближчий not-found.tsx. generateStaticParams вказує Next.js, які slug-и попередньо рендерити як статичний HTML, що прибирає серверний рендеринг при кожному запиті і покращує TTFB.

Catch-all маршрут для документації з опціональним коренем

tsx
// app/docs/[[...slug]]/page.tsx // Збігається з: /docs, /docs/react, /docs/react/hooks/useState export default function Docs({ params }: { params: { slug?: string[] } }) { if (!params.slug || params.slug.length === 0) { return <h1>Головна документації</h1> } const path = params.slug.join('/') // /docs/react/hooks → path = "react/hooks" return <div>Документ: /{path}</div> }

Подвійні дужки [[...slug]] роблять усі сегменти опціональними. Один файл обробляє і індексну сторінку документації, і будь-який вкладений шлях. З одинарними дужками [...slug] індексна сторінка давала б 404.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?