Hogyan használj párhuzamos és lehallgatott útvonalakat a Next.js App Routerben: megosztható modálok, dashboardok és a default.js buktatói gyakorlati példákkal.
A párhuzamos útvonalak (parallel routes) és a lehallgatott útvonalak (intercepting routes) a Next.js App Router két kiegészítő routing primitívje: az előbbi több, független oldalrészletet renderel ugyanazon az URL-en a @folder slot-konvencióval, az utóbbi pedig elkapja a navigációt, és a célt egy másik útvonal layoutján belül jeleníti meg (tipikusan modálként), miközben a teljes oldal-frissítés esetén az eredeti oldal töltődik be. Pages-router gyakorlatból érkezve ez elsőre kicsit varázslatnak tűnik, de pár példa után kattan, és olyan UI-mintákat enged, amik korábban kliensoldali állapotgépeket igényeltek.
A párhuzamos útvonalakat a @slot nevű mappák jelölik, és a szülő layout külön propként kapja meg őket, független loading.tsx és error.tsx állapotokkal.
A default.js fájl kritikus: ha hiányzik, soft navigáció után 404-et kapsz a slotokban, hard refresh esetén pedig az egész oldal eldobható.
A lehallgatott útvonalak konvenciói: (.) azonos szint, (..) egy szint feljebb, (..)(..) kettő, (...) a gyökértől.
A klasszikus „kép-modál" minta (Instagram, Dribbble) párhuzamos és lehallgatott útvonalak kombinációjával készül: modal kliensoldali állapot nélkül, megosztható URL-lel.
Párhuzamos útvonalakkal feltételes UI is építhető. Egy szerepkör-alapú dashboard akár három különböző slot-tartalmat is mutathat ugyanazon az URL-en.
A lehallgatás csak soft navigáció (kliensoldali Link kattintás) esetén aktiválódik. Közvetlen URL-megnyitás vagy oldal-újratöltés a tényleges célt rendereli.
Mi a párhuzamos útvonal a Next.js-ben?
A párhuzamos útvonal egy olyan App Router-konstrukció, amely lehetővé teszi, hogy egyetlen layout több, egymástól függetlenül renderelődő szegmenst tartalmazzon. Pages-router idejéből érkezve ezt úgy érdemes elképzelni, hogy a _app.tsx helyett van egy layoutod, ami nemcsak children-t, hanem további propokat (@team, @analytics, @notifications) is kap, és mindegyik prop saját React fa, saját loading állapottal, saját error boundary-vel.
A megnevezés azért „párhuzamos", mert az adott URL-en a page.tsx és minden slot egyszerre, függetlenül renderelődik a szerveren. Ha az egyik slot lassú (mondjuk 800 ms-os adatlekérés), a másik slot azonnal megjeleníthető, amíg az első <Suspense>-szel streamelődik. Ez a minta különösen olyan dashboardoknál ér valamit, ahol a sidebar, a fő tartalom és a notification panel egymástól függetlenül szeretne adatot kérni. Lásd korábbi cikkemet a React Server Components streaming és párhuzamos lekérési mintáiról.
A Next.js 15.3 óta a párhuzamos útvonalak hivatalosan stabilak (lásd a parallel routes hivatalos dokumentációt), de a működő példák még mindig ritkák. A Stack Overflow-on rengeteg „default.js missing" jellegű kérdés van, amik 2024–2025-ös tutorialok elavult kódjából származnak.
A @folder slot-konvenció és a layout props
A párhuzamos útvonalat egy @-tal kezdődő mappa hozza létre az App Router fa megfelelő szintjén. A mappanév adja a layout-prop nevét: app/@team/page.tsx esetén a app/layout.tsx egy team nevű propot kap, és a TypeScriptes layout-aláírás így néz ki:
app/
├── layout.tsx
├── page.tsx // ez a children
├── @team/
│ ├── page.tsx
│ ├── loading.tsx
│ └── default.tsx
└── @analytics/
├── page.tsx
├── loading.tsx
└── default.tsx
Két dolgot érdemes megjegyezni. Egyrészt a children implicit slot: nincs hozzá @children mappa, ez a hagyományos route-fa. Másrészt a @ mappa nem kerül bele az URL-be. A /dashboard URL meghívja a @team/page.tsx-et és a @analytics/page.tsx-et a háttérben, de az URL továbbra is /dashboard. A felhasználó nem tudja, hogy három különálló szerveroldali React fát rendereltél össze.
A default.js szerepe és a leggyakoribb buktatók
A default.tsx (vagy default.js) az a fallback, amelyet a Next.js akkor renderel egy slotban, ha az aktuális URL-hez nincs explicit match. Ennek két szcenárió miatt van jelentősége: a hard refresh és a soft navigáció után átirányított slot-állapotok miatt.
Tegyük fel, hogy van egy /dashboard route a @analytics slottal, és van egy /dashboard/settings aloldal, ami szintén children-en keresztül renderelődik, de a @analytics alatt nincs settings/page.tsx. Ha a felhasználó kliensoldalon (Link) navigál ide, a @analytics a memóriájában tartja a korábbi state-et. De ha a böngészőben F5-öt nyom, a Next.js nem tudja, mit rendereljen a @analytics slotba, és itt jön be a default.tsx. Ha hiányzik, 404 érkezik.
// app/@analytics/default.tsx
// Fallback, amikor az URL-hez nem tartozik explicit @analytics tartalom
export default function AnalyticsDefault() {
return null; // vagy egy üres placeholder UI
}
Az én pages-router beidegződésem évekig az volt, hogy a 404-et a route fa szintjén kezelem. Az App Routerben a slot-szintű default.tsx miatt át kellett gondolnom: a slotok egy URL alatt is hiányozhatnak, nem csak az egész oldal. Honestly, ezt az egy mintát megérteni gyorsabb volt, mint a fél nap, amit én elvesztegettem rá a kezdetekkor.
Mi a lehallgatott útvonal és hogyan működik?
A lehallgatott útvonal (intercepting route) egy másik szegmens renderelését kapja el, de csak akkor, ha a felhasználó kliensoldali navigációval (egy <Link> kattintással) ér oda. Közvetlen URL beírás vagy window.location esetén a tényleges, „mély" route renderelődik.
A konvenció zárójeles előtag, ami a route fában elfoglalt relatív pozíciót jelöli:
Konvenció
Mit jelent
Példa cél
(.)folder
Azonos szegmens-szint
Egy feed/ alatti lista egy item-modálba
(..)folder
Egy szinttel feljebb
feed/photos alól a feed/users elfogása
(..)(..)folder
Két szinttel feljebb
Mélyebb beágyazott útvonalakhoz
(...)folder
A app gyökérből
Globális modálok bárhonnan
Fontos félreértés-veszély: a zárójeles előtag itt nem ugyanaz, mint a (group) mappák, amik route grouping-ra valók. Az (.) egy hivatalos lehallgatási token, a (group) csak organizáció.
A működés alapja a Next.js softNavigation algoritmusa: a kliensoldali router figyeli a Link kattintásokat, és ha a célhoz létezik egy lehallgatott megfelelő, akkor azt rendereli az aktuális layout fán belül a tényleges célszegmens helyett. Az URL azonban a tényleges célra változik. Ez azt jelenti, hogy a felhasználó megoszthatja a linket, és aki közvetlen URL-lel nyitja meg, a teljes oldalt látja.
Megosztható kép-modál párhuzamos és lehallgatott útvonalakkal
Ez a kanonikus példa, és érdemes minden részletét megérteni. A cél: van egy fotó-feed, és ha a felhasználó rákattint egy képre, modál nyílik a részletekkel, de az URL /feed/photo/42-re vált, hogy megosztható és bookmarkolható legyen. Hard refresh esetén a teljes részletek-oldal renderelődik. Pages routerben ezt React Portal, custom history kezelés és scroll-position hackek nélkül nehéz volt megcsinálni.
A @modal/default.tsx-t ne felejtsd el, return null elég:
// app/@modal/default.tsx
export default function ModalDefault() {
return null;
}
Ez a teljes flow. Soft navigáció (<Link href="/photo/42">) esetén az URL /photo/42 lesz, a @modal slot betölti a modált, a children ott marad a feed oldalon. Direct URL esetén a app/photo/[id]/page.tsx teljes oldal renderelődik. Megosztáskor a kontextus megőrződik.
Párhuzamos dashboard analitikai szegmensekkel
Egy másik valós példa, amit mostanában éles projektben használtam: egy admin dashboard, ahol a fő tartalom (KPI grafikonok), a sidebar (gyors statisztikák) és a notification panel teljesen független adatforrásokból táplálkozik. Pages routerben mindezt SWR-rel és kliens-oldali state-tel kötöttem volna össze; App Routerben mindhárom szerverkomponens, és mindhárom párhuzamosan renderelődik.
Mindegyik slot saját loading.tsx-t kap, ezért a böngészőben látható lesz, ahogyan a gyors KPI azonnal megjelenik, a notification panel skeleton-t mutat 200 ms-ig, a stats sidebar pedig 600 ms-ig. Mind szerveroldali stream a React Suspense API-n keresztül. A felhasználó nem várja meg a leglassabb lekérdezést ahhoz, hogy az oldal interaktívvá váljon.
Ha az adatlekérési oldalt mélyebben szeretnéd érteni, ajánlom a Drizzle ORM és App Router integrációs cikket. Onnan tudod, hogyan oszd szét a queryket úgy, hogy ne legyen véletlen N+1 a slotokon keresztül (ezzel a hibával én magam is sokáig küzdöttem az első dashboard-iterációban).
Feltételes párhuzamos útvonalak szerepkör szerint
Egy ritkábban használt, de erős minta: a layout maga dönti el, melyik slotot rendereli. Mivel a slotok propként érkeznek, a layout-ban feltételes JSX-szel kapcsolhatod őket:
Itt fontos: a render nem azt jelenti, hogy a másik slot szerveroldali query-je nem fut le. A Next.js mindkét slotot előkészítheti, ezért szerepkör-érzékeny adatokat a slotokon belül is ellenőrizz, ne csak a layoutban. A hitelesítés helyes mintáit a Auth.js v5 cikkemben részleteztem.
Mikor érdemes párhuzamos vagy lehallgatott útvonalat használni?
A két primitív gyakran együtt jár, de külön is hasznosak. Pár konkrét döntési pont, ami a saját éles munkám során kialakult:
Párhuzamos útvonal jó választás: dashboard több, független adatforrással; tabos UI, ahol minden tab saját szerver-adatot kér; szerepkör-alapú feltételes regions; oldalak független loading.tsx / error.tsx állapotokkal.
Lehallgatott útvonal jó választás: kép-modálok, részlet-popoverek, „quick view" minták, ahol akarjuk a megosztható URL-t, de nem akarjuk a kontextus elvesztését; multi-step űrlapok, ahol a következő lépés modálban nyílik, de mély-linkkel teljes oldal lesz.
Ne használj párhuzamos útvonalat, ha a slotok kliens-state-en keresztül kommunikálnak. Ez ellentétes a primitív szándékával, és a React context-szel mérhetetlenül egyszerűbb.
Ne használj lehallgatott útvonalat, ha a modálnak nincs URL-jelentősége. A sima kliens-modál (egyetlen useState) sokkal egyszerűbb és nem terheli a routert.
Ha űrlap-modálokat tervezel, érdemes elolvasni a Server Actions űrlapok cikket. A lehallgatott útvonal és a useActionState kombinációja különösen szép, mert a Server Action közvetlenül kezeli a modál bezárását redirect()-tel.
Tipikus hibák és hibakeresési minták
A három leggyakoribb hiba, amit kollégáknál és magamnál is láttam (az utolsó kettőbe személyesen szaladtam bele az első éles projektnél):
404 hard refresh után: hiányzó default.tsx
A leggyakoribb. A tünet: minden szépen működik, amíg <Link>-kel navigálsz, de F5-re az egyik slot 404-et ad, vagy az egész oldal eltűnik. A javítás minden slotba egy default.tsx:
export default function Default() {
return null;
}
Lehallgatás nem aktiválódik
Ha a modál a teljes oldalra navigál a Link helyett, ellenőrizd: (1) a zárójeles előtag pontosan stimmel-e (a (.) és a (..) könnyű elgépelni); (2) a Link nem-e <a> hagyományosan; (3) nem-e window.location.href = ...-zal navigálsz, mert az hard refresh.
Modál bezárása újratölti a hátteret
Ha router.back() helyett router.push("/feed")-et hívsz a modál bezárásakor, az új navigáció elveszi a slot kontextusát. Mindig router.back() kell ahhoz, hogy a háttér érintetlen maradjon.
Gyakran ismételt kérdések
Mi a különbség a párhuzamos és a lehallgatott útvonalak között?
A párhuzamos útvonal több, egymástól független szegmenst renderel egy URL-en a @slot konvencióval. A lehallgatott útvonal egy másik szegmens megjelenését kapja el soft navigáció esetén, és az aktuális layout fán belül rendereli (jellemzően modálként). Gyakran együtt használjuk őket: a lehallgatott útvonal egy párhuzamos slotba renderel.
Miért kell default.js a párhuzamos útvonalakhoz?
Mert hard refresh vagy olyan URL-szegmensre navigálás esetén, amelyhez a slot alatt nincs explicit page.tsx, a Next.js nem tudja, mit rendereljen a slotba. A default.tsx ad fallback UI-t; ha hiányzik, 404-et vagy üres oldalt kapsz.
Hogyan készítsek megosztható modált intercepting route-tal?
Hozz létre egy @modal párhuzamos slotot a layoutban, és benne egy (.)photo/[id] lehallgatott szegmenst. A részletek-oldal külön app/photo/[id]/page.tsx alatt él. Soft navigáció a modált tölti a @modal slotba, hard refresh a teljes oldalt rendereli ugyanazon a megosztható URL-en.
Lehet párhuzamos útvonalat feltételesen renderelni?
Igen. A slotok propként érkeznek a layoutba, így JSX-feltételeken belül választhatod ki, melyik rendereljen. Vigyázz: a Next.js mindkét slot szerveroldali kódját előkészítheti, ezért szerepkör-érzékeny lekérdezéseket a slot page.tsx-én belül is védd le.
Lehallgatott útvonal működik mobil böngészőben back gombbal?
Igen. A böngésző back gombja a router.back()-hez hasonlóan a history-stack előző bejegyzésére lép, ami visszaállítja az eredeti URL-t és bezárja a modált. Ez egyébként az egyik fő érv a párhuzamos és lehallgatott megoldás mellett a klasszikus kliens-modállal szemben.