Parallel Routes a Intercepting Routes v Next.js 16: Modální okna s deep linkingem

Parallel Routes a Intercepting Routes v Next.js 16 umožňují vykreslit modální okna s deep linkingem. Praktický vzor, default.tsx fallback, soft vs. hard navigace a workaround pro HMR chybu v 16.0–16.2.

Parallel Routes v Next.js 16: Modály (2026)

Aktualizováno: 28. června 2026

Parallel Routes a Intercepting Routes jsou dvě konvence souborového systému v Next.js 16 App Routeru, které společně umožňují vykreslit jakoukoli stránku jako modální okno se sdílitelnou URL. Uživatel klikne na odkaz, otevře se overlay, ale po obnovení stránky se obsah načte jako plnohodnotná samostatná stránka. V kombinaci řeší tři klasické bolesti modálních oken: deep linking, zachování kontextu po reloadu a předvídatelné chování tlačítek zpět/vpřed v prohlížeči.

  • Parallel Routes vytváří pojmenované sloty pomocí konvence @folder. Sloty se vykreslují paralelně ve stejném layoutu a každý má vlastní stav načítání i chybový boundary.
  • Intercepting Routes ((.), (..), (..)(..), (...)) přepisují cíl navigace pouze při soft navigaci. Reload nebo přímý vstup vykreslí cílovou stránku normálně.
  • Každý slot, který nemá page.tsx pro aktuální URL, vyžaduje default.tsx. Jinak Next.js vrátí 404 po reloadu.
  • V Next.js 16 (březen 2026) existuje známá chyba HMR, která akumuluje (.) značky při ukládání souboru. Vyžaduje restart dev serveru.
  • Drahá data fetch operace patří do nadřazeného layoutu nebo do plnohodnotné stránky, ne do modálního slotu. Jinak je při každém otevření modálu zbytečně refetchujete.
  • useSelectedLayoutSegment('modal') přečte aktivní segment slotu (užitečné pro podmíněné zavírání overlay nebo pro analytics).

Co jsou Parallel Routes v Next.js?

Parallel Routes umožňují vykreslit více nezávislých stránkových segmentů ve stejném layoutu. Definujete je konvencí @slotName. Složka, jejíž název začíná zavináčem, se v sourozeneckém layout.tsx objeví jako React prop. Každý slot má vlastní cestu načítání, vlastní error boundary a vlastní historii. Pro modální okna nás zajímá především to, že slot může být na dané URL prázdný, a stejný layout pak vypadá úplně stejně jako bez něj.

Tady je praktický příklad. Root layout, který vedle hlavního obsahu drží slot pro modální okno:

// app/layout.tsx
export default function RootLayout({
  children,
  modal,
}: {
  children: React.ReactNode
  modal: React.ReactNode
}) {
  return (
    <html lang="cs">
      <body>
        {children}
        {modal}
        <div id="modal-root" />
      </body>
    </html>
  )
}

Klíčové pravidlo, které mě stálo den ladění: sloty fungují jen v layout.tsx, ne v page.tsx. Pokud chcete modální okno omezené na konkrétní sekci (třeba jen /dashboard), musíte slot deklarovat v app/dashboard/layout.tsx, ne v page.tsx stejné složky. Vercel to v dokumentaci zmiňuje, ale snadno se to přehlédne. Chybová hláška není moc nápověda.

Sloty také čtou vlastní stav přes useSelectedLayoutSegment('modal'). Honestly, hodí se to víc, než byste čekali. Třeba když chcete v hlavičce ztmavit pozadí jen pokud je modál otevřený, nebo když logujete analytics události typu „user opened modal X".

Jak fungují Intercepting Routes?

Intercepting Routes přesměrují cíl navigace, aniž by změnily URL v adresním řádku. Konvence používá závorky s tečkami podobně jako relativní cesty v souborovém systému: (.) zachytí sourozence, (..) zachytí o úroveň výš, (..)(..) o dvě úrovně výš a (...) zachytí cestu od kořene app/. Pozor, počítají se segmenty cesty, ne segmenty souborového systému, takže (.) a sourozenec ve stejné složce se ne vždy překrývá s tím, co byste čekali.

Konkrétně. Pokud máte stránku /photos/123 a chcete, aby se po kliknutí ze seznamu /feed vykreslila jako modál v rámci /feed, vytvoříte app/feed/@modal/(..)photos/[id]/page.tsx. Next.js intercept zapne jen při klientské navigaci přes <Link> nebo router.push(). Pokud uživatel zadá /photos/123 přímo do adresního řádku nebo zmáčkne F5, intercept se přeskočí a vykreslí se app/photos/[id]/page.tsx jako plnohodnotná stránka.

Tohle dvojí chování je celý důvod, proč pattern existuje. Pro hlubší kontext o tom, jak App Router zachází s routováním a jak na něj navazují další konvence, doporučuju migraci middleware na proxy.ts v Next.js 16. Proxy běží před jakoukoli interception, takže autentizační redirecty se chovají konzistentně v obou režimech.

Dejme tomu, že máme galerii fotek. Cíle: kliknutí v gridu otevře fotku v overlay s URL /photos/123, reload téže URL ukáže fotku jako samostatnou stránku, tlačítko zpět zavře overlay. Tady je kompletní struktura:

app/
├── layout.tsx                       // deklaruje slot @modal
├── page.tsx                         // grid s fotkami (Link na /photos/[id])
├── @modal/
│   ├── default.tsx                  // vrací null, když není modál aktivní
│   └── (.)photos/
│       └── [id]/
│           └── page.tsx             // modální verze
└── photos/
    └── [id]/
        └── page.tsx                 // standalone verze (deep link, reload)

Modální komponenta. Všimněte si, že je to client component, protože potřebuje router.back():

// app/@modal/(.)photos/[id]/page.tsx
'use client'

import { useRouter } from 'next/navigation'
import { use } from 'react'

export default function PhotoModal({
  params,
}: {
  params: Promise<{ id: string }>
}) {
  const router = useRouter()
  const { id } = use(params)

  return (
    <div
      role="dialog"
      aria-modal="true"
      className="fixed inset-0 z-50 grid place-items-center bg-black/70"
      onClick={() => router.back()}
    >
      <div
        className="rounded bg-white p-6"
        onClick={(e) => e.stopPropagation()}
      >
        {/* Drahá data fetchnout v server child komponentě, ne tady */}
        <PhotoDetail id={id} />
        <button onClick={() => router.back()}>Zavřít</button>
      </div>
    </div>
  )
}

Standalone stránka app/photos/[id]/page.tsx je úplně normální server component. Žádný overlay, plné rozložení stránky. Sdílenou logiku (fetch fotky, validace ID) vytáhněte do utility funkce a importujte do obou. Vyhnete se duplicitě a zachováte konzistentní cache klíče. Pokud načítáte fotku přes fetch() s cache: 'force-cache', oba renderery dostanou stejnou odpověď z Next.js Image cache.

default.tsx a problém s 404

Tohle je nejčastější bod, kde modal pattern padá. Když uživatel obnoví stránku na /about, Next.js neumí „uhádnout", co má vykreslit ve slotu @modal. Předchozí klientský stav je pryč. Pokud slot nemá default.tsx, vrátí se 404 pro celou stránku, ne jen pro slot.

Řešení je naštěstí jednoduché. Do každého slotu přidejte default.tsx, který vrací null:

// app/@modal/default.tsx
export default function Default() {
  return null
}

Pokud používáte parallel/intercepting routy v hluboce vnořených dynamických segmentech, samotný default.tsx někdy nestačí. Oficiální dokumentace Next.js k parallel routes doporučuje přidat catch-all segment [...rest]/page.tsx mimo slot. Pokryje hard navigaci na URL, kterou jste nepředvídali.

Deep linking a soft vs. hard navigace

Rozdíl mezi soft a hard navigací je tady kritický a hodně lidí ho podceňuje. Soft navigace probíhá uvnitř React stromu. Komponenty <Link> a programatické router.push() ji spustí. Next.js diffne stromy, vymění jen změněné segmenty a sloty zachovají svůj klientský stav. Intercept se aktivuje jen tady.

Hard navigace je všechno ostatní: F5, vložení URL do nového tabu, window.location, externí odkaz. Server dostane normální request a vykreslí cílovou URL od nuly. Intercept slot ignoruje a vykreslí app/photos/[id]/page.tsx jako standalone. Tohle je přesně to chování, které chcete pro sdílení odkazů. Někdo dostane URL na Slacku, klikne, vidí celou stránku, ne useknutý overlay nad cizím kontextem.

V praxi to znamená dvě věci. Za prvé, sdílená logika musí být opravdu sdílená. Jakákoli divergence mezi modálem a standalone stránkou bude pro uživatele matoucí. Za druhé, pokud potřebujete v modálu data, která standalone stránka nemá (např. odkaz na předchozí fotku v seznamu), čerpejte je z layoutu nad slotem, ne ze samotného modálu. Layout je sdílený a stabilní mezi oběma režimy.

Výkon a data fetching v modálu

Když jsem tenhle pattern poprvé nasadil, naivně jsem do modálu nacpal fetch() pro celou fotku včetně komentářů a EXIF dat. Výsledek: každé kliknutí na fotku znovu načítalo seznam komentářů, protože layout se neměnil, ale modální slot se mountoval. Klasická chyba. Modální slot není stabilní layout, je to page segment, který se přemountuje při každé změně URL.

Druhá vrstva optimalizace je streaming. Pokud modál ukazuje fotku, komentáře a související fotky, oba bloky stáhnete paralelně přes <Suspense>, a uživatel uvidí fotku okamžitě, zatímco komentáře streamují. Detailně jsem to popsal v článku Streaming a Suspense v Next.js 16. Stejné principy platí i pro intercepting routy.

Třetí věc. Sdílejte cache mezi modálem a standalone verzí pomocí fetch() dedupingu. Když oba renderery volají fetch('/api/photo/123') se stejnými parametry během stejného requestu, Next.js volá API jen jednou. Pokud potřebujete delší cache mezi requesty, použijte direktivu use cache z Cache Components. Viz průvodce Cache Components a Partial Prerendering.

Známá chyba HMR v Next.js 16

Pokud děláte první nasazení tohoto patternu v Next.js 16.0–16.2, narazíte na frustrující chybu. Po každém uložení souboru v dev módu se intercept routa „akumuluje". Next.js postupně přidává další (.) značky, až dev server vrátí Error: Invalid interception route. Detaily v Issue #91265 na GitHubu.

Workaround, než se to opraví: po každé úpravě souboru ve slotu restartujte dev server. Není to elegantní, ale produkční build se chová správně. Chyba je čistě v setup-dev-bundler.js. Pokud máte velký projekt a restart trvá dlouho, dočasně vypněte HMR pro modální slot tím, že soubor přepíšete kompletně místo inkrementálních úprav. Uložení page.tsx jako celku nereprodukuje chybu tak spolehlivě.

Časté chyby a jak se jim vyhnout

Pět věcí, na které jsem narazil nebo o nich slyšel od kolegů migrujících z pages routeru:

  1. Slot v page.tsx místo layout.tsx. Nefunguje, nepadá s rozumnou chybou, prostě se nevykreslí. Vždy v layoutu.
  2. Chybějící default.tsx. Stránka funguje, dokud někdo neudělá F5. Pak 404 celé aplikace. Přidávejte default.tsx hned při vytváření slotu.
  3. Drahá fetch v modálu. Refetchuje se při každém otevření. Vytáhněte do layoutu nad slotem.
  4. Více modálních slotů zároveň. Pokud máte @modal a @auth, oba se ukážou současně, pokud oba mají aktivní obsah. Pro modal switching použijte jeden slot a discriminator (např. ?modal=auth jako query param).
  5. Závislost na router.back() pro zavření. Funguje jen pokud byl modál otevřen klientskou navigací. Pokud uživatel přišel přes deep link, router.back() ho odhodí na předchozí doménu. Použijte router.replace('/parent-route') jako fallback.

Modal pattern je jeden z mocnějších nástrojů App Routeru, ale potřebuje disciplínu kolem default.tsx a data flow. Když máte tyhle dvě věci pod kontrolou, dostanete UX, který v pages routeru vyžadoval ručně psaný stavový stroj a synchronizaci s URL přes shallow: true hack. Já jsem v pages routeru tohle psal třikrát. App Router verze je jednodušší o tři vrstvy.

Často kladené otázky

Jaký je rozdíl mezi parallel routes a intercepting routes?

Parallel Routes vykreslují více segmentů paralelně ve stejném layoutu pomocí slotů @folder. Intercepting Routes mění cíl navigace za běhu. Pomocí konvence (.), (..), (...) přesměrují klientskou navigaci na jinou souborovou cestu. Pro modal pattern používáte obojí dohromady: slot drží overlay, intercept rozhoduje, jestli se cílová stránka vykreslí v něm nebo standalone.

Proč dostávám 404 při reloadu stránky s parallel routes?

Slot nemá default.tsx pro aktuální URL. Při soft navigaci si Next.js pamatuje, co bylo v slotu, ale při hard navigaci (F5) tu informaci ztratí a potřebuje fallback. Přidejte default.tsx, který vrací null, do každého slotu, včetně implicitního children slotu, pokud má layout asymetrickou strukturu.

Jak se modál chová při sdílení URL?

Sdílená URL se po vložení do prohlížeče otevře jako standalone stránka (app/photos/[id]/page.tsx), ne jako modál. Intercept se aktivuje pouze při klientské navigaci uvnitř aplikace. To je úmyslné chování. Uživatel, který přijde z externího odkazu, nemá kontext „rodičovské" stránky, takže standalone verze je správná.

Mohu mít více modálních oken současně?

Technicky ano. Vytvoříte více slotů (@modal, @drawer) a každý nezávisle interceptuje různé cesty. V praxi to vede ke zmatkům: oba sloty se mohou aktivovat zároveň a překrývat se. Lepší pattern je jeden slot s discriminatorem v URL (např. query param) a podmíněným renderováním uvnitř.

Funguje tento pattern v produkčním buildu?

Ano, intercepting i parallel routes jsou plně podporované v produkci. HMR chyba v Next.js 16.0–16.2 (Issue #91265) se týká pouze dev serveru. Produkční build kompiluje routy jednou a problém nereprodukuje. Před deployem si ale projděte všechny stavy: open modal, reload v modálu, deep link, back/forward navigaci.

Ben Howard
O Autorovi Ben Howard

Full-stack Next.js developer who's been with the framework since pages-only days. Slowly warming up to App Router.