Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Маршрутизація в Next.js (маршрутизація на основі файлів)». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Маршрутизація на основі файлів у Next.js** будує URL зі структури папок директорії `app`. Кожен `page.tsx` робить ту папку доступною як маршрут. ``` app/page.tsx → / app/docs/page.tsx → /docs app/blog/[slug]/page.tsx → /blog/:slug ``` **Ключове:** папка без `page.tsx` не створює маршрут.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Маршрутизація на основі файлів (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.tsx` | UI сторінки (робить сегмент доступним як маршрут) | | `layout.tsx` | Спільний макет, зберігається між навігаціями | | `loading.tsx` | Suspense-заглушка під час завантаження даних | | `error.tsx` | Error boundary для сегмента | | `not-found.tsx` | Кастомна 404 для сегмента | | `template.tsx` | Як layout, але перемонтується при кожній навігації | | `route.ts` | API-ендпоінт (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.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.