Паралельні та перехоплені маршрути у Next.js 16: модалки, дашборди й багатослотні лейаути (2026)
Практичний посібник з паралельних і перехоплених маршрутів у Next.js 16: модалки через deep-link, багатослотні дашборди, умовний рендер і streaming на слот. З типовими помилками та їх діагностикою.
Паралельні маршрути у Next.js 16 — це механізм App Router, який дозволяє рендерити кілька незалежних сторінок у межах одного лейауту через іменовані слоти (папки з префіксом @), а перехоплені маршрути ((.), (..), (...)) підмінюють цільовий маршрут у поточному контексті. Типовий приклад: відкриття деталей запису як модального вікна без повного переходу. Разом ці два примітиви покривають десь 90% сценаріїв багатопанельних дашбордів, вкладених модалок і умовних лейаутів, які раніше доводилося писати на клієнті через стан і портали.
Паралельні маршрути позначаються папками @slotName усередині сегменту та рендеряться як окремі пропси лейауту разом із children.
Файл default.js обовʼязковий для кожного слота, інакше жорсткі перезавантаження після soft-навігації повертають 404.
Префікси перехоплення: (.) це той самий рівень, (..) на рівень вище, (..)(..) на два рівні вище, (...) від кореня app/.
Класичний паттерн «модалка через deep-link»: паралельний слот @modal плюс перехоплення (.)photo/[id], який залишає фонову сторінку живою.
Кожен слот має власні loading.tsx і error.tsx, тож streaming й ізоляція помилок працюють незалежно.
У Next.js 16 паралельні слоти повністю сумісні з Partial Prerendering і dynamicIO: статичні слоти пре-рендеряться, динамічні стрімляться.
Що таке паралельні маршрути в Next.js?
Паралельні маршрути дозволяють оголосити кілька незалежних дерев маршрутизації в межах одного лейауту. У класичному App Router лейаут отримує один проп children, і на цьому історія закінчується. У Next.js 16, коли ви створюєте всередині сегменту папку з префіксом @ (наприклад, @analytics), фреймворк генерує додатковий проп із тим самим іменем і передає його в той самий layout.tsx. Це означає, що URL /dashboard одночасно активує три піддерева: базове children, @analytics і @team. Кожне з них має власний page.tsx, loading.tsx, error.tsx і навіть власний not-found.tsx.
Для стаф-інженерів це критична зміна ментальної моделі. Маршрутизатор більше не мапить URL на одну сторінку, а на композицію сторінок. Такий підхід усуває цілий клас багів, які виникали, коли ми ліпили паралельні панелі через клієнтський стан у ProviderContext: стрибки, дублювання запитів, втрата стану при перезавантаженні. Паралельні слоти зберігають і кешують стан URL для кожного піддерева окремо, а React 19 Server Components дозволяють рендерити їх узагалі без гідрації, якщо всередині нема інтерактиву. Офіційна документація Parallel Routes у Next.js детально описує внутрішню механіку, зокрема поведінку Router Cache.
Анатомія слота: конвенції @folder і default.js
Розгляньмо мінімальний приклад дашборду з двома слотами. Дерево файлів виглядає так:
default.tsx — це фолбек, який Next рендерить, коли слот не має активного стану в URL. Він потрібен для двох сценаріїв: (1) жорсткого перезавантаження після soft-навігації, під час якої слот змінив свій сегмент, і (2) навігації між сегментами, де слот взагалі не існує. Якщо default.tsx відсутній, ви отримаєте 404 при F5. Чесно кажучи, це топ-1 баг у продакшені у команд, які тільки-но мігрували з pages/. Я на нього наступав тричі за перший місяць після виходу Next 16. Мінімальний фолбек часто виглядає як export default function Default() { return null }, але для дашбордів варто рендерити скелетон, щоб уникнути layout shift.
Перехоплені маршрути: (.), (..), (...)
Перехоплення маршруту — це підміна цільового URL у поточному навігаційному контексті. Коли користувач клікає посилання <Link href="/feed/photo/42">, звичайний App Router рендерить app/feed/photo/[id]/page.tsx. Але якщо у сегменті app/feed/ є папка (.)photo/[id]/, Next «перехопить» перехід і покаже саме її лише при soft-навігації. Пряме відкриття або F5 віддадуть повний маршрут, тому deep-link продовжує працювати як зазвичай.
Синтаксис префіксів має чітку семантику рівнів:
Префікс
Значення
Типовий кейс
(.)
Той самий сегмент
Модалка деталей у тій самій колонці
(..)
Один сегмент вище
Перехоплення сусіднього маршруту
(..)(..)
Два сегменти вище
Модалка з глибоко вкладеного списку
(...)
Від кореня app/
Глобальна модалка (логін, шорткати)
Важливо: рахуються сегменти маршруту, а не рівні файлової системи. Групи роутів у дужках (наприклад, (marketing)) не рахуються за сегмент, і це найчастіший ступор при діагностиці. Деталі є в довіднику Intercepting Routes, який я раджу тримати відкритим у сусідній вкладці, коли будуєте перше перехоплення.
Як створити модальне вікно через deep-link
Це найпоширеніший продакшен-паттерн, і водночас той, який найважче правильно скомпонувати з першого разу. Мета проста: клік на карточку у стрічці відкриває деталі як модалку поверх стрічки, refresh на URL деталей віддає повну сторінку, а кнопка «назад» закриває модалку.
Компонент модалки повинен уміти закриватися через router.back(), щоб історія браузера залишалася коректною:
// app/@modal/(.)photo/[id]/page.tsx
"use client"
import { useRouter } from "next/navigation"
export default function PhotoModal({ params }: { params: { id: string } }) {
const router = useRouter()
return (
<dialog open onClose={() => router.back()} className="modal">
<button onClick={() => router.back()} aria-label="Закрити">×</button>
{/* Тут можна тягнути дані Server Component-ом через <Suspense> */}
<PhotoDetail id={params.id} />
</dialog>
)
}
Дашборд із незалежними табами
Другий продакшен-кейс, який я регулярно бачу у SaaS-адмінках, це багатопанельний дашборд, де кожна панель має власний фільтр і pagination, синхронізований з URL. До паралельних маршрутів це вимагало велетенського контекст-провайдера й ручного мерджу query-params. Тепер усе набагато простіше:
Кожен слот тримає власний сегмент і кешується Router Cache незалежно. Хук useSelectedLayoutSegment('users') віддає активний сегмент конкретного слота, що дозволяє малювати активний таб-хайлайт без клієнтського стану. Комбінація паралельних маршрутів із Partial Prerendering у Next.js 16 дає прогресивний рендер: shell і статичні слоти віддаються миттєво, а динамічні дошиваються стрімом. У парі з директивою dynamicIO ви явно контролюєте, які слоти можуть бути пре-рендерені, а які ні.
Умовний рендер слотів за роллю користувача
Слот у лейауті це звичайний React node, тому нічого не заважає обгорнути його в перевірку прав. Класичний приклад: адмін бачить панель модерації, звичайний користувач її не бачить. У Next.js 16 з появою proxy.ts (заміна middleware.ts, деталі у нашому гіді з міграції на proxy.ts) можна безпечно прочитати сесію в лейауті і показати або приховати слот:
Кожен слот має власне дерево loading.tsx, error.tsx, not-found.tsx. React 19 автоматично обгортає слот у окремий <Suspense>-boundary, тому пізніші дані не блокують інші панелі. На практиці це означає, що дашборд із трьох слотів (@users тягне з Redis, @revenue з Postgres, @funnel з data warehouse із 900 мс латенси) віддає перші два практично миттєво і стрімить третій. Що показувати в loading.tsx? Скелетон розміром із фінальний контент, щоб уникнути CLS. Що НЕ показувати? Спінер по центру всього слота: це створює візуальний шум під час паралельного стрімінгу.
error.tsx у слоті ловить помилки лише свого піддерева. Це дає можливість зробити «граційний фолбек»: навіть якщо один слот впав, решта дашборду залишається робочою. Обовʼязково додайте кнопку reset(), щоб користувач міг перепробувати без перезавантаження цілої сторінки. Разом із Turbopack-білдами Next.js 16 цей паттерн дає найкращу локальну DX: HMR оновлює лише активний слот, а не все дерево.
Моя рекомендація для команд така. Якщо на панель робиться fetch і треба URL-shareable стан, беріть паралельні маршрути завжди. Якщо це ефемерний UI-стан (розкрита-згорнута секція, hover-tooltip), клієнтський стан цілком нормально працює. Роут-групи ((group)) залиште для організації файлів, а не для рантайм-поведінки.
Поширені помилки та як їх діагностувати
За рік поставок цієї фічі в клієнтських проєктах я зібрав топ-5 симптомів, які повторюються з разу в раз:
404 після F5 на модалці. Забули default.tsx у слоті @modal. Додайте export default function Default() { return null } і забудьте.
Перехоплення не спрацьовує. Ви перерахували рівні ((..) замість (.)) або кладете перехоплення в роут-групу. Next групи не рахує за сегмент. Перевірте дерево у виводі next build.
Модалка «моргає» на deep-link. Ви одночасно рендерите фон і перехоплений маршрут. Розділіть їх: @modal/(.)photo/[id]/page.tsx для soft-nav, окрема photo/[id]/page.tsx для прямого відкриття.
Слоти дублюють запити. Обидва слоти незалежно тягнуть спільний ресурс. Виносьте його у shared cache-функцію з unstable_cache або використовуйте новий use cache. Деталі є в матеріалі про директиву use cache.
Помилка «You cannot have two parallel pages that resolve to the same path». Ви створили @slot/page.tsx, який мапиться на той самий URL, що й children. Явно винесіть його на під-URL або перевірте, що children-сторінка не існує.
Часті запитання
Чи можна використовувати паралельні маршрути без App Router?
Ні. Паралельні маршрути це виключно фіча App Router. У Pages Router аналога не існує, там доведеться використовувати клієнтський стан або кастомний роутер поверх Next Router.
Як передати дані з батьківського лейауту до слота?
Слоти отримують дані так само, як і сторінки: через params, searchParams, серверні fetch-и або cookies()/headers(). Прямої prop-drilling з батьківського лейауту немає, це навмисне рішення команди Next. Спільні дані виносьте у cache-функцію й імпортуйте в кожному слоті, React 19 dedup дедуплікує виклики.
Чи впливають паралельні маршрути на SEO?
Ні, якщо ви правильно налаштували default.tsx і повні маршрути для deep-link. Google краулить URL так само, як звичайні маршрути, а перехоплений варіант рендериться лише при soft-навігації, коли вже є контекст користувача. Головне: переконайтеся, що прямий URL модалки віддає повноцінну сторінку з коректним generateMetadata.
Чи можна вкладати паралельні маршрути один в одного?
Так. Кожен слот це самостійне дерево, всередині якого можна оголосити свої власні @subslot. Це корисно для складних дашбордів із дрилдаунами, але тримайте глибину до 2 або 3 рівнів, інакше візуалізація дерева стає болючою для команди.
Як тестувати перехоплені маршрути?
Юніт-тестів на самі компоненти сторінки достатньо. Для E2E я рекомендую Playwright: клік на <Link> має показати модалку без повного перезавантаження (перевіряйте, що не відбувся load-event), а page.goto на той самий URL має віддати повну сторінку. Обидва сценарії тримайте як окремі тести.