Párhuzamos és lehallgatott útvonalak Next.js App Routerben (2026): modálok, dashboardok, default.js

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.

Next.js párhuzamos útvonalak (2026)

Frissítve: 2026. június 26.

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
import type { ReactNode } from "react";

export default function RootLayout({
  children,
  team,
  analytics,
}: {
  children: ReactNode;
  team: ReactNode;
  analytics: ReactNode;
}) {
  return (
    <html lang="hu">
      <body>
        <header>{team}</header>
        <main>{children}</main>
        <aside>{analytics}</aside>
      </body>
    </html>
  );
}

A megfelelő mappastruktúra:

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
(.)folderAzonos szegmens-szintEgy feed/ alatti lista egy item-modálba
(..)folderEgy szinttel feljebbfeed/photos alól a feed/users elfogása
(..)(..)folderKét szinttel feljebbMélyebb beágyazott útvonalakhoz
(...)folderA app gyökérbőlGlobá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 struktúra:

app/
├── layout.tsx
├── @modal/
│   ├── default.tsx
│   └── (.)photo/
│       └── [id]/
│           └── page.tsx     // modál tartalom
├── photo/
│   └── [id]/
│       └── page.tsx         // teljes oldal részletek
└── feed/
    └── page.tsx             // kép-rács Link-ekkel

A root layout:

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

A modál tartalma (figyeld, hogy ez egy szerver komponens, ami a data fetch-et közvetlenül a renderben végzi):

// app/@modal/(.)photo/[id]/page.tsx
import { getPhoto } from "@/lib/photos";
import { Modal } from "@/components/modal";

export default async function PhotoModal({
  params,
}: {
  params: Promise<{ id: string }>;
}) {
  const { id } = await params;
  const photo = await getPhoto(id);

  return (
    <Modal>
      <h2>{photo.title}</h2>
      <img src={photo.url} alt={photo.alt} />
      <p>{photo.description}</p>
    </Modal>
  );
}

A Modal kliens komponens kezeli az ESC-billentyűt és a backdrop kattintást. Bezáráskor router.back()-et hív, ami visszanavigál a /feed-re:

"use client";
// components/modal.tsx
import { useRouter } from "next/navigation";
import { useEffect, useRef } from "react";
import { createPortal } from "react-dom";

export function Modal({ children }: { children: React.ReactNode }) {
  const router = useRouter();
  const ref = useRef<HTMLDivElement>(null);

  useEffect(() => {
    const handler = (e: KeyboardEvent) => {
      if (e.key === "Escape") router.back();
    };
    document.addEventListener("keydown", handler);
    return () => document.removeEventListener("keydown", handler);
  }, [router]);

  const root = document.getElementById("modal-root");
  if (!root) return null;

  return createPortal(
    <div
      role="dialog"
      aria-modal="true"
      className="modal-backdrop"
      onClick={(e) => e.target === ref.current && router.back()}
      ref={ref}
    >
      <div className="modal-content">{children}</div>
    </div>,
    root
  );
}

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.

app/
└── dashboard/
    ├── layout.tsx
    ├── page.tsx              // children (fő KPI)
    ├── @stats/
    │   ├── page.tsx
    │   ├── loading.tsx
    │   └── default.tsx
    └── @notifications/
        ├── page.tsx
        ├── loading.tsx
        └── default.tsx
// app/dashboard/layout.tsx
export default function DashboardLayout({
  children,
  stats,
  notifications,
}: {
  children: React.ReactNode;
  stats: React.ReactNode;
  notifications: React.ReactNode;
}) {
  return (
    <div className="grid grid-cols-[240px_1fr_320px] gap-4">
      <aside>{stats}</aside>
      <main>{children}</main>
      <aside>{notifications}</aside>
    </div>
  );
}

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:

// app/dashboard/layout.tsx
import { getCurrentUser } from "@/lib/auth";

export default async function DashboardLayout({
  children,
  admin,
  user,
}: {
  children: React.ReactNode;
  admin: React.ReactNode;
  user: React.ReactNode;
}) {
  const me = await getCurrentUser();
  const role = me?.role ?? "guest";

  return (
    <>
      <main>{children}</main>
      {role === "admin" ? admin : user}
    </>
  );
}

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.

Ben Howard
A Szerzőről Ben Howard

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