Warum Parallel Routes und Intercepting Routes euer Routing-Game verändern
Hand aufs Herz: Wer hat schon mal versucht, in einer Next.js-Anwendung ein Modal zu bauen, das eine eigene URL hat, beim Refresh nicht verschwindet und sich bei der Zurück-Navigation sauber schließt? Ja, genau — der Schmerz ist real. Klassische State-basierte Modale lösen vielleicht eines dieser Probleme, schaffen dafür aber drei neue.
Parallel Routes und Intercepting Routes im App Router lösen das elegant auf Dateisystem-Ebene. Kein zusätzlicher State, keine Router-Hacks. Einfach ein deklaratives Muster, das sich wie native Navigation anfühlt und trotzdem modale UX liefert.
In diesem Artikel bauen wir gemeinsam ein vollständiges Modal-System mit Deep Linking, schauen uns Dashboard-Layouts mit unabhängigen Panels an und räumen mit den häufigsten Fallstricken auf. Also, los geht's.
Was sind Parallel Routes?
Parallel Routes erlauben es euch, mehrere unabhängige Routen-Segmente gleichzeitig im selben Layout zu rendern. Stellt euch das wie mehrere Browser-Tabs vor — aber innerhalb einer einzigen Seite. Klingt erstmal abstrakt, wird aber schnell konkret.
Die @slot-Konvention
Next.js nutzt die Namenskonvention @ordnername, um sogenannte Slots zu definieren. Ein Slot ist im Grunde ein benannter Bereich im Layout, der seinen eigenen Seiteninhalt, Loading-States und Error-Boundaries haben kann.
app/
├── layout.tsx # Empfängt @analytics und @team als Props
├── page.tsx # Der "children"-Slot (implizit)
├── @analytics/
│ ├── page.tsx # Wird parallel gerendert
│ └── default.tsx # Fallback bei nicht-matchender URL
└── @team/
├── page.tsx # Wird parallel gerendert
└── default.tsx # Fallback bei nicht-matchender URL
Im Layout empfangt ihr die Slots als Props — genau wie children:
// app/layout.tsx
export default function DashboardLayout({
children,
analytics,
team,
}: {
children: React.ReactNode;
analytics: React.ReactNode;
team: React.ReactNode;
}) {
return (
<div className="grid grid-cols-2 gap-4">
<main>{children}</main>
<aside>{analytics}</aside>
<section>{team}</section>
</div>
);
}
Unabhängiges Streaming und Error Handling
Hier kommt der eigentliche Clou: Jeder Slot kann seinen eigenen loading.tsx und error.tsx haben. Wenn die Analytics-Daten mal wieder ewig brauchen, sieht der User trotzdem sofort das Team-Panel. Kein Slot blockiert die anderen — und das macht in der Praxis einen riesigen Unterschied.
app/
├── @analytics/
│ ├── page.tsx
│ ├── loading.tsx # Eigener Skeleton für Analytics
│ └── error.tsx # Eigene Fehlerbehandlung
└── @team/
├── page.tsx
├── loading.tsx # Eigener Skeleton für Team
└── error.tsx # Eigene Fehlerbehandlung
Was sind Intercepting Routes?
Intercepting Routes erlauben es, eine Route in einem anderen Kontext zu rendern als dem, in dem sie normalerweise erscheinen würde. Das klassische Beispiel kennt ihr wahrscheinlich von Instagram: Ein Foto im Feed wird beim Klick als Modal geöffnet — aber die URL ändert sich trotzdem auf /photo/123.
Ziemlich elegant, oder?
Die Klammer-Konvention
Die Intercepting-Konvention nutzt Klammern mit Punkten, ähnlich wie relative Pfade im Dateisystem:
(.)ordner— fängt Routen auf der gleichen Ebene ab(..)ordner— fängt Routen eine Ebene höher ab(..)(..)— fängt Routen zwei Ebenen höher ab(...)— fängt Routen vom Root der App ab
Wichtig: Die (..)-Konvention basiert auf Routen-Segmenten, nicht auf dem Dateisystem. Das heißt: @slot-Ordner zählen nicht als Segment. Ich hab selbst mal eine halbe Stunde an genau diesem Punkt debuggt, bevor der Groschen gefallen ist.
Wie Intercepting in der Praxis funktioniert
Bei einer Soft Navigation (also ein Klick auf einen <Link>) wird die abgefangene Route im aktuellen Layout gerendert — zum Beispiel als Modal. Bei einer Hard Navigation (direkter URL-Aufruf oder Browser-Refresh) wird die vollständige Seite geladen. Ihr bekommt also das Beste aus beiden Welten.
Das Modal-Pattern: Parallel Routes + Intercepting Routes kombinieren
Jetzt wird's spannend. Wenn ihr beide Konzepte kombiniert, könnt ihr Modale bauen, die sich wie echte Seiten verhalten — mit eigener URL, Browser-History-Support und Deep Linking. Das ist meiner Meinung nach eines der besten Features im gesamten App Router.
Schritt 1: Die Ordnerstruktur anlegen
Nehmen wir an, ihr habt einen Feed mit Fotos. Ein Klick auf ein Foto soll ein Modal öffnen, aber /photo/[id] soll auch als eigenständige Seite funktionieren:
app/
├── layout.tsx
├── page.tsx # Feed-Seite
├── @modal/
│ ├── default.tsx # Gibt null zurück (Modal geschlossen)
│ └── (.)photo/
│ └── [id]/
│ └── page.tsx # Foto als Modal
└── photo/
└── [id]/
└── page.tsx # Foto als vollständige Seite
Schritt 2: Das Layout mit dem Modal-Slot erweitern
// app/layout.tsx
export default function RootLayout({
children,
modal,
}: {
children: React.ReactNode;
modal: React.ReactNode;
}) {
return (
<html lang="de">
<body>
{children}
{modal}
</body>
</html>
);
}
Schritt 3: Den default.tsx anlegen
Der default.tsx im @modal-Slot ist entscheidend — ohne ihn funktioniert gar nichts. Er stellt sicher, dass das Modal nicht gerendert wird, wenn es nicht aktiv ist:
// app/@modal/default.tsx
export default function ModalDefault() {
return null;
}
Schritt 4: Die Modal-Komponente bauen
// app/@modal/(.)photo/[id]/page.tsx
'use client';
import { useRouter } from 'next/navigation';
export default function PhotoModal({
params,
}: {
params: Promise<{ id: string }>;
}) {
const router = useRouter();
const { id } = use(params);
return (
<div
className="fixed inset-0 bg-black/60 z-50 flex items-center justify-center"
onClick={() => router.back()}
>
<div
className="bg-white rounded-lg max-w-2xl w-full p-6"
onClick={(e) => e.stopPropagation()}
>
<h2>Foto {id}</h2>
<img src={`/photos/${id}.jpg`} alt={`Foto ${id}`} />
<button onClick={() => router.back()}>Schließen</button>
</div>
</div>
);
}
Schritt 5: Die vollständige Fotoseite
Das hier ist die Seite, die bei einem direkten URL-Aufruf oder Refresh angezeigt wird:
// app/photo/[id]/page.tsx
export default async function PhotoPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
return (
<main className="max-w-4xl mx-auto p-8">
<h1>Foto {id}</h1>
<img src={`/photos/${id}.jpg`} alt={`Foto ${id}`} />
<p>Hier stehen Details, Kommentare und Metadaten zum Foto.</p>
</main>
);
}
Schritt 6: Links im Feed setzen
Damit die Intercepting-Logik greift, müssen Links mit der <Link>-Komponente von Next.js gesetzt werden. Ein normales <a>-Tag löst eine Hard Navigation aus — und dann war's das mit dem Modal.
// app/page.tsx (Feed)
import Link from 'next/link';
export default function FeedPage() {
const photoIds = [1, 2, 3, 4, 5];
return (
<div className="grid grid-cols-3 gap-4 p-8">
{photoIds.map((id) => (
<Link key={id} href={`/photo/${id}`}>
<img src={`/photos/${id}.jpg`} alt={`Foto ${id}`} />
</Link>
))}
</div>
);
}
Wenn der User jetzt auf ein Foto klickt, passiert Folgendes: Die URL wechselt zu /photo/3, das Modal öffnet sich über dem Feed, und bei einem Refresh wird die vollständige Fotoseite geladen. Genau das Verhalten, das Instagram, Twitter und Co. verwenden. Ziemlich cool, wenn man bedenkt, wie wenig Code dafür nötig ist.
Praxisbeispiel: Dashboard mit unabhängigen Panels
Parallel Routes eignen sich aber nicht nur für Modale. Ein richtig starker Anwendungsfall ist ein Dashboard, bei dem verschiedene Bereiche unabhängig voneinander Daten laden und Fehler behandeln können.
app/dashboard/
├── layout.tsx
├── page.tsx
├── @revenue/
│ ├── page.tsx # Umsatzdaten (Server Component)
│ ├── loading.tsx # Skeleton-Loader
│ └── error.tsx # Fehler nur für Revenue
├── @orders/
│ ├── page.tsx # Bestellungen (Server Component)
│ ├── loading.tsx # Eigener Skeleton
│ └── error.tsx # Fehler nur für Orders
└── @notifications/
├── page.tsx # Benachrichtigungen
└── loading.tsx # Eigener Skeleton
// app/dashboard/layout.tsx
export default function DashboardLayout({
children,
revenue,
orders,
notifications,
}: {
children: React.ReactNode;
revenue: React.ReactNode;
orders: React.ReactNode;
notifications: React.ReactNode;
}) {
return (
<div className="grid grid-cols-3 gap-6 p-8">
<div className="col-span-2">
{children}
<div className="grid grid-cols-2 gap-4 mt-6">
{revenue}
{orders}
</div>
</div>
<aside>{notifications}</aside>
</div>
);
}
Der Vorteil ist sofort spürbar: Wenn die Umsatzdaten drei Sekunden brauchen, sieht der User trotzdem sofort seine Bestellungen und Benachrichtigungen. Jedes Panel streamt unabhängig und zeigt seinen eigenen Loading-State. Für Dashboards mit mehreren Datenquellen ist das ein absoluter Gamechanger.
Die häufigsten Fehler — und wie ihr sie vermeidet
Ich hab in verschiedenen Projekten so ziemlich jeden dieser Fehler mindestens einmal gemacht. Damit euch das erspart bleibt, hier die größten Stolperfallen:
Fehler 1: Fehlende default.tsx — die 404-Falle
Das ist mit Abstand der häufigste Fehler. Nach einem Seiten-Refresh oder einer Hard Navigation versucht Next.js, den aktiven Zustand jedes Slots zu bestimmen. Findet es keinen passenden page.tsx und keinen default.tsx, gibt es einen 404.
Die Regel ist simpel: Jeder @slot-Ordner braucht auf jeder Routen-Ebene entweder einen passenden page.tsx oder einen default.tsx als Fallback.
// Der einfachste default.tsx — gibt einfach nichts zurück
export default function Default() {
return null;
}
Fehler 2: Slots in page.tsx statt layout.tsx verwenden
Parallel Route Slots funktionieren ausschließlich in layout.tsx. Wenn ihr versucht, einen @slot als Prop in page.tsx zu empfangen, wird er einfach ignoriert. Keine Fehlermeldung, kein Hinweis — er wird einfach nicht gerendert. Das kann einen schon mal in den Wahnsinn treiben.
Fehler 3: Bindestriche im Slot-Namen
Da der Slot-Name direkt als JavaScript-Variable im Layout verwendet wird, solltet ihr Bindestriche vermeiden. @user-settings führt zu Problemen — nutzt stattdessen @userSettings oder @usersettings.
Fehler 4: a-Tags statt Link verwenden
Hatten wir schon kurz erwähnt, aber es lohnt sich, das nochmal zu betonen: Intercepting Routes funktionieren nur bei Soft Navigation über die <Link>-Komponente. Ein normaler <a>-Tag löst eine Hard Navigation aus — und das Modal wird komplett übergangen.
Fehler 5: Neuen Slot im Dev-Server nicht erkannt
Wenn ihr einen neuen @slot-Ordner anlegt, erkennt der Next.js Dev-Server diesen manchmal nicht automatisch. Die Lösung ist simpel (wenn auch nervig): Dev-Server neu starten.
# Dev-Server stoppen und neu starten
# Ctrl+C, dann:
npm run dev
Tipps für die Produktion
Route Groups für isolierte Modale
Wenn ihr Modale nur auf bestimmten Seiten braucht, nutzt Route Groups, um Slots zu isolieren. So vermeidet ihr, dass ein Modal-Slot global in jedem Layout verfügbar sein muss:
app/
├── (shop)/
│ ├── layout.tsx # Hat @cart-Slot
│ ├── @cart/
│ │ ├── default.tsx
│ │ └── (.)cart/
│ │ └── page.tsx # Warenkorb als Modal
│ └── products/
│ └── page.tsx
└── (blog)/
├── layout.tsx # Kein @cart — nicht nötig
└── posts/
└── page.tsx
Bedingte Slots mit Authentifizierung
Ein Pattern, das ich persönlich richtig gerne nutze: Parallel Routes ermöglichen bedingtes Rendering basierend auf der Authentifizierung — direkt im Layout als Server Component. Kein Client-seitiges Flackern, keine zusätzlichen API-Calls.
// app/layout.tsx
import { getSession } from '@/lib/auth';
export default async function Layout({
children,
admin,
user,
}: {
children: React.ReactNode;
admin: React.ReactNode;
user: React.ReactNode;
}) {
const session = await getSession();
return (
<>
{children}
{session?.role === 'admin' ? admin : user}
</>
);
}
Performance-Überlegungen
Ein Punkt, den man im Hinterkopf behalten sollte: Intercepting Routes können zu duplizierten JavaScript-Bundles führen. Die Modal-Version und die vollständige Seitenversion teilen sich unter Umständen Code, der doppelt ausgeliefert wird.
Bei kleinen Komponenten ist das kein Thema. Bei großen, datenintensiven Seiten lohnt es sich aber, die Modal-Version bewusst schlank zu halten und nur die wichtigsten Informationen anzuzeigen.
Häufig gestellte Fragen
Was ist der Unterschied zwischen Parallel Routes und normalen verschachtelten Routen?
Verschachtelte Routen rendern ihren Inhalt innerhalb der Eltern-Route — es gibt immer nur einen aktiven Inhalt pro Segmentebene. Parallel Routes dagegen rendern mehrere Inhalte gleichzeitig und unabhängig voneinander im selben Layout. Jeder Slot hat seinen eigenen Loading-State und Error-Boundary und kann separat navigiert werden, ohne die anderen zu beeinflussen.
Funktionieren Parallel Routes mit React Server Components?
Ja, absolut. Die einzelnen Slots können asynchrone Server Components sein, die unabhängig voneinander Daten laden und parallel gestreamt werden. Das ist ehrlich gesagt einer der größten Vorteile: Jeder Slot kann seine eigene Datenquelle haben, ohne dass die anderen darauf warten müssen.
Warum bekomme ich einen 404-Fehler nach einem Seiten-Refresh mit Parallel Routes?
Bei einem Hard Refresh kann Next.js den aktiven Zustand eines Slots nicht aus der Browser-History ableiten. Wenn für den aktuellen URL-Pfad kein passender page.tsx im Slot existiert, sucht Next.js nach einem default.tsx. Fehlt auch dieser, wird ein 404 angezeigt. Die Lösung: Legt in jedem @slot-Ordner auf jeder Routenebene eine default.tsx-Datei an, die null zurückgibt.
Kann ich mehrere Modale gleichzeitig mit Intercepting Routes öffnen?
Technisch ja — aber ehrlich gesagt würde ich davon abraten. Wenn ihr mehrere @slot-Ordner mit Intercepting Routes anlegt, werden alle passenden Slots gleichzeitig gerendert. Das führt in der Praxis zu überlagerten Modalen, und die UX ist dann eher so mittel. Besser: Nutzt einen einzelnen @modal-Slot und rendert darin unterschiedliche Inhalte basierend auf der Route.
Sind Parallel Routes und Intercepting Routes auch in Next.js 16 verfügbar?
Ja, beide Features sind seit Next.js 13.3 verfügbar und werden in Next.js 16 vollständig unterstützt. Mit Next.js 15 und 16 wurden einige Bugs behoben, insbesondere bei der Kombination von Intercepting Routes mit dynamischen Segmenten. Meine Empfehlung: Nutzt mindestens Next.js 15 oder neuer für eine stabile Erfahrung.