Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Паралельні маршрути та перехоплення маршрутів у Next.js». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Паралельні маршрути** (parallel routes) дозволяють відображати кілька незалежних сторінок в одному Next.js layout через іменовані слоти `@folder`, кожен з власними станами завантаження і помилок. **Перехоплюючі маршрути** (intercepting routes) використовують синтаксис `(.)folder` щоб захопити навігацію до дочірнього сегмента і показати модалку замість нього. ```tsx // app/feed/@modal/(.)photo/[id]/page.tsx export default async function PhotoModal({ params }) { const { id } = await params; const photo = await getPhoto(id); return <Modal><img src={photo.url} alt={photo.title} /></Modal>; } ``` **Ключове:** паралельні маршрути для незалежних панелей з окремим завантаженням даних, перехоплюючі для модалок з власним URL які закриваються кнопкою «назад».Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Паралельні маршрути** (parallel routes) відображають кілька незалежних сторінок в одному layout через іменовані слоти `@folder`. Перехоплюючі маршрути (intercepting routes) захоплюють навігацію до дочірнього сегмента і рендерять модальне вікно замість нього, тоді як URL або залишається батьківським, або змінюється на перехоплений шлях - залежно від того, як користувач туди потрапив. ## Теорія ### TL;DR - Паралельні маршрути використовують синтаксис `@folder` і передаються в layout як окремі React-пропси - Кожен слот має власні `loading.tsx` і `error.tsx` та фетчить дані незалежно - Перехоплюючі маршрути використовують `(.)folder`, `(..)folder` або `(...)folder` щоб захопити навігацію до відповідного сегмента - Паралельні - для мультипанельних дашбордів, перехоплюючі - для модалок, які закриваються кнопкою «назад» - Разом вони реалізують Instagram-паттерн: модалка при клієнтській навігації, повна сторінка при прямому відкритті URL ### Короткий приклад ```tsx // app/dashboard/layout.tsx export default function DashboardLayout({ children, stats, notifications, }: { children: React.ReactNode; stats: React.ReactNode; // app/dashboard/@stats/page.tsx notifications: React.ReactNode; // app/dashboard/@notifications/page.tsx }) { return ( <div className="flex h-screen"> <aside className="w-80 border-r">{stats}</aside> <div className="flex-1 flex flex-col"> <header>{notifications}</header> <main>{children}</main> </div> </div> ); } ``` Слоти `stats` і `notifications` фетчать дані паралельно. Якщо `stats` кидає помилку, `notifications` і `children` продовжують рендеритись - за умови що в кожного слота є власний `error.tsx`. ### Головна різниця Паралельні маршрути заповнюють іменовані слоти з папок-сусідів на одному URL-рівні. Вони рендеряться одночасно, кожен як окреме дерево React Server Component з незалежним завантаженням даних. Перехоплюючі маршрути не додають нового місця в макеті. Вони перехоплюють рендеринг для конкретного сегмента і показують свій компонент замість нього, а отже той самий URL може відображати два різних інтерфейси залежно від того, чи прийшов користувач туди за посиланням чи ввів адресу напряму. ### Коли що використовувати - Мультипанельний дашборд де панелі звертаються до різних API: паралельні маршрути - Модальне вікно або дравер, який відкривається поверх сторінки і закривається кнопкою «назад»: перехоплюючі маршрути - Умовний layout для авторизованих і неавторизованих користувачів: паралельні маршрути з умовним рендерингом - Оверлей пошуку як модалка за посиланням, але повна сторінка при прямому відкритті: комбінація обох Прості двоколонкові макети вирішуються через CSS без паралельних маршрутів. Перехоплюючі маршрути не потрібні для модалок без власного URL. ### Як працюють слоти Папки з `@` на початку - це слоти. Next.js передає їх як пропси до найближчого батьківського layout. Ім'я папки стає іменем пропса: `@stats` стає пропсом `stats`. ``` app/dashboard/ layout.tsx <- отримує { children, stats, notifications } page.tsx <- стає children @stats/ page.tsx <- стає пропсом stats loading.tsx <- показується тільки коли stats завантажується error.tsx <- ловить помилки тільки в stats @notifications/ page.tsx <- стає пропсом notifications error.tsx ``` З власного досвіду: якщо забути `default.tsx` в слоті, Next.js кидає 404 при переході на URL де в слота немає відповідного сегмента. Достатньо додати `default.tsx` що повертає `null`. ### Перехоплюючі маршрути: синтаксис і рівні Префікс визначає на якому рівні відбувається перехоплення: ``` (.)photo - перехоплює сегмент на тому ж рівні (..)photo - перехоплює на один рівень вище (..)(..)photo - на два рівні вище (...)photo - перехоплює від кореня app ``` Класичний приклад: сторінка `/feed` має слот `@modal`. Всередині нього `(.)photo/[id]/page.tsx` перехоплює навігацію до `/photo/[id]` при переходах з `/feed`. URL змінюється на `/photo/123`, але Next.js рендерить компонент модалки замість повної сторінки. Якщо відкрити `/photo/123` напряму, перехоплення не спрацьовує і завантажується повна сторінка. ### Таблиця порівняння | Характеристика | Паралельні маршрути | Перехоплюючі маршрути | |---|---|---| | Синтаксис | `@slot/page.tsx` | `(.)slot/page.tsx`, `(..)slot/page.tsx` | | Рендеринг | Кілька слотів в layout одночасно | Замінює рендеринг дочірнього сегмента | | Поведінка URL | Кожен слот відповідає своєму шляху | URL змінюється, але рендеринг перевизначається | | Завантаження даних | Повністю незалежне для кожного слота | Використовує контекст батьківського layout | | Ізоляція помилок | Власний `error.tsx` для кожного слота | Потрібен `error.tsx` у папці перехоплення | | Де застосовувати | Дашборди, умовна авторизація | Фотомодалки, логін-оверлеї, панелі пошуку | ### Як це працює всередині На етапі збірки Next.js сканує папки з `@` і генерує пропси слотів для layout-ів як частину файлової конвенції App Router. Під час навігації RSC-запити для кожного слота запускаються паралельно без waterfall-ефекту. Перехоплюючі маршрути працюють через пріоритет збігу сегментів: `(.)` відповідає поточному сегменту шляху, `(..)` піднімається на один рівень вище, а перехоплений компонент призупиняє рендеринг дочірнього маршруту до моменту закриття. ### Поширені помилки **Відсутній `default.tsx` в слоті** ```tsx // app/dashboard/@stats/ не має default.tsx // Перехід на /dashboard/settings -> Next.js видає 404, // бо @stats не має сегмента для цього URL // Рішення: додати app/dashboard/@stats/default.tsx export default function StatsDefault() { return null; } ``` **Відсутній `loading.tsx` для кожного слота** Без `loading.tsx` в кожному слоті окремо повільний слот блокує відображення всього layout. Додай `@stats/loading.tsx` і `@notifications/loading.tsx` незалежно. Кожен стає власною Suspense-межею. **Перехоплення без префікса `(.)`** ``` app/settings/profile/page.tsx <- звичайний дочірній маршрут, змінює URL, перехоплення немає app/settings/(.)profile/page.tsx <- перехоплює /settings/profile і показує модалку ``` Без `(.)` Next.js обробляє це як звичайний вкладений маршрут. URL змінюється і модалка ніколи не з'явиться. **Немає ізоляції помилок у перехоплюючому маршруті** ```tsx // app/settings/(.)profile/page.tsx // Якщо userId недійсний, без error.tsx в цій папці весь layout впаде const user = await fetchUser(searchParams.userId); ``` Додай `app/settings/(.)profile/error.tsx` щоб помилка оброблялась локально. Батьківський layout залишиться недоторканим. **Посилання напряму на URL слота** `<Link href="/dashboard/@stats">` трактує папку `@stats` як реальний URL-сегмент, що ламає роутинг. Слоти - це пропси layout, а не навігаційні шляхи. ### Де використовується в реальних проектах - Vercel dashboard: паралельні слоти для аналітики і деталей використання, кожен звертається до окремого API - Linear: перехоплюючі маршрути для модального вікна швидкого пошуку поверх будь-якого проектного view - GitHub: паралельні слоти для readme і issues репозиторія в одному layout - Instagram-style feed: канонічний приклад з документації Next.js, де слот `@modal` комбінується з `(.)photo/[id]` - Supabase dashboard: перехоплюючі модалки для SQL-редактора поверх таблиць ### Питання на співбесіді **Q:** Чим відрізняється завантаження даних у паралельному слоті від вкладеної сторінки? **A:** Кожен слот запускає власний `fetch()` як незалежний RSC без waterfall-ефекту. Вкладена сторінка чекає на fetch батьківського layout перед тим як рендерити дочірній компонент. **Q:** Який URL залишається при відкритті модалки через перехоплюючий маршрут? **A:** URL змінюється на перехоплений шлях (наприклад `/photo/123`), але layout рендерить компонент модалки. При прямому відкритті цього URL перехоплення не спрацьовує і завантажується повна сторінка. **Q:** Що станеться якщо паралельний слот кидає помилку і не має `error.tsx`? **A:** Помилка піде вгору до найближчої межі помилок. Без `error.tsx` для кожного слота окремо весь layout впаде. З ним тільки цей слот покаже UI помилки. **Q:** Чи можна комбінувати паралельні та перехоплюючі маршрути? **A:** Так, і це рекомендований паттерн для фотомодалок. Слот `@modal` живе в layout стрічки, а всередині нього `(.)photo/[id]` перехоплює навігацію на сторінку фото. Слот керує станом модалки, перехоплення відповідає за збіг сегментів. **Q:** Senior: Як `default.tsx` впливає на стрімінг у паралельних слотах і коли він його блокує? **A:** `default.tsx` заповнює слот коли для поточного URL немає відповідного сегмента. Next.js рендерить його одразу, без стрімінгу. Якщо `default.tsx` містить важкий компонент або повільний імпорт, він блокує стрімінг слота навіть коли основний контент вже готовий. Використовуй `default.tsx` тільки для простих `null`-повернень або легких скелетонів. ## Приклади ### Дашборд з незалежними панелями Дашборд у стилі Vercel, де бічна панель статистики завантажується і обробляє помилки незалежно від основного контенту. ```tsx // app/dashboard/layout.tsx export default function DashboardLayout({ children, stats, notifications, }: { children: React.ReactNode; stats: React.ReactNode; notifications: React.ReactNode; }) { return ( <div className="flex h-screen"> <aside className="w-80 border-r p-4">{stats}</aside> <div className="flex-1 flex flex-col"> <header className="border-b p-4">{notifications}</header> <main className="flex-1 p-6">{children}</main> </div> </div> ); } // app/dashboard/@stats/page.tsx export default async function Stats() { const data = await fetch("https://api.example.com/stats", { cache: "no-store", }).then((r) => r.json()); return ( <div> <p>Запити: {data.requests}</p> <p>Помилки: {data.errors}</p> </div> ); } // app/dashboard/@stats/error.tsx "use client"; export default function StatsError() { return <p>Не вдалось завантажити статистику.</p>; } // app/dashboard/@stats/loading.tsx export default function StatsLoading() { return <div className="animate-pulse h-20 bg-gray-100 rounded" />; } ``` Кожен слот фетчить незалежно. Якщо API статистики недоступний, основний графік і сповіщення продовжують рендеритись. ### Фотомодалка з перехоплюючими маршрутами Instagram-паттерн: клієнтська навігація показує модалку, пряме відкриття URL завантажує повну сторінку. ```tsx // Структура файлів: // app/feed/page.tsx -> /feed (сітка фото) // app/feed/@modal/(.)photo/[id]/page.tsx -> перехоплює /photo/[id] з /feed // app/feed/@modal/default.tsx -> null (без модалки за замовчуванням) // app/photo/[id]/page.tsx -> /photo/123 (повна сторінка при прямому відкритті) // app/feed/layout.tsx export default function FeedLayout({ children, modal, }: { children: React.ReactNode; modal: React.ReactNode; }) { return ( <> {children} {modal} </> ); } // app/feed/@modal/(.)photo/[id]/page.tsx export default async function PhotoModal({ params, }: { params: Promise<{ id: string }>; }) { const { id } = await params; const photo = await getPhoto(id); return ( <div className="fixed inset-0 bg-black/50 flex items-center justify-center"> <div className="bg-white rounded-lg p-4 max-w-2xl"> <img src={photo.url} alt={photo.title} className="w-full" /> <h2 className="mt-2 text-lg font-medium">{photo.title}</h2> </div> </div> ); } // app/photo/[id]/page.tsx export default async function PhotoPage({ params, }: { params: Promise<{ id: string }>; }) { const { id } = await params; const photo = await getPhoto(id); return ( <div className="max-w-4xl mx-auto"> <img src={photo.url} alt={photo.title} className="w-full" /> <h1 className="mt-4 text-2xl font-bold">{photo.title}</h1> <p className="mt-2 text-gray-600">{photo.description}</p> </div> ); } // app/feed/@modal/default.tsx export default function ModalDefault() { return null; } ``` Клік на фото у стрічці веде на `/photo/123`. Префікс `(.)photo` перехоплює цей перехід всередині layout стрічки і рендерить модалку. Пряме відкриття `/photo/123` обходить перехоплення і завантажує повну сторінку. ### Умовний layout для авторизованих користувачів ```tsx // app/layout.tsx import { getUser } from "@/lib/auth"; export default async function RootLayout({ children, dashboard, login, }: { children: React.ReactNode; dashboard: React.ReactNode; // app/@dashboard/page.tsx login: React.ReactNode; // app/@login/page.tsx }) { const user = await getUser(); // Обидва слоти - повноцінні компоненти зі своїми даними. // Layout лише вирішує який з них показати. return ( <html lang="uk"> <body>{user ? dashboard : login}</body> </html> ); } ``` Обидва слоти `@dashboard` і `@login` мають власне завантаження даних і межі помилок. Layout лише вирішує який з них відобразити на основі перевірки сесії.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.