Εισαγωγή: Γιατί Χρειάζεστε Parallel και Intercepting Routes
Ας είμαστε ειλικρινείς — τα σύγχρονα web interfaces έχουν γίνει πολύ πιο σύνθετα από ό,τι πριν λίγα χρόνια. Dashboards με πολλαπλά ανεξάρτητα panels, modals που εμφανίζονται χωρίς να χάσεις το context της σελίδας, tab groups που φορτώνουν διαφορετικό περιεχόμενο — όλα αυτά απαιτούν μια αρχιτεκτονική που ξεπερνά τις δυνατότητες του κλασικού routing.
Αν έχετε δοκιμάσει να διαχειριστείτε πολλαπλά ανεξάρτητα τμήματα σε μία σελίδα με τον παραδοσιακό τρόπο, πιθανότατα ξέρετε τον πόνο: πολύπλοκο state management, prop drilling, και κώδικας που γίνεται δύσκολα συντηρήσιμος. Τα modals υλοποιούνταν συνήθως με global state ή context providers, χάνοντας τη δυνατότητα shareable URLs και ιστορικού πλοήγησης.
Το Next.js App Router (στις εκδόσεις Next.js 15 και 16) προσφέρει δύο εργαλεία που λύνουν αυτά τα προβλήματα με κομψό τρόπο: τα Parallel Routes και τα Intercepting Routes. Μαζί, σας δίνουν τη δυνατότητα να φτιάξετε σύνθετα UI patterns — Instagram-style photo modals, dashboards με ανεξάρτητα panels, conditional layouts — με πλήρη υποστήριξη URL routing, streaming, και error handling.
Λοιπόν, ας δούμε πώς δουλεύουν στην πράξη.
Τι Είναι τα Parallel Routes στο Next.js
Τα Parallel Routes σας επιτρέπουν να εμφανίζετε ταυτόχρονα μία ή περισσότερες σελίδες μέσα στο ίδιο layout. Κάθε "τμήμα" ονομάζεται slot και ορίζεται με τη σύμβαση @folder. Τα slots περνιούνται ως props στο parent layout component, ακριβώς όπως το γνωστό children.
Τα βασικά χαρακτηριστικά τους:
- Named slots: Ορίζονται με τη σύμβαση
@folder(π.χ.@analytics,@team). - Props στο layout: Κάθε slot γίνεται prop στο parent layout component.
- Δεν επηρεάζουν το URL: Τα slots δεν προσθέτουν segments στο URL — είναι καθαρά UI-level separation.
- Implicit slot: Το
childrenprop είναι ένα implicit slot, ισοδύναμο με@children.
Πώς Ορίζονται τα Slots
Για να δημιουργήσετε parallel routes, χρησιμοποιείτε φακέλους με πρόθεμα @ μέσα στο ίδιο route segment. Η δομή αρχείων μοιάζει κάπως έτσι:
app/
├── layout.tsx
├── page.tsx
├── @analytics/
│ ├── page.tsx
│ └── default.tsx
└── @team/
├── page.tsx
└── default.tsx
Στο παράδειγμα αυτό, το layout.tsx δέχεται τα slots ως props:
// app/layout.tsx
export default function RootLayout({
children,
analytics,
team,
}: {
children: React.ReactNode;
analytics: React.ReactNode;
team: React.ReactNode;
}) {
return (
<html lang="el">
<body>
<div className="flex flex-col gap-4">
{/* Κύριο περιεχόμενο — implicit slot */}
<main>{children}</main>
{/* Parallel slots */}
<div className="grid grid-cols-2 gap-4">
<section>{analytics}</section>
<section>{team}</section>
</div>
</div>
</body>
</html>
);
}
Κάθε slot μπορεί να έχει τα δικά του loading.tsx, error.tsx, ακόμα και nested layouts — πλήρως ανεξάρτητα το ένα από το άλλο. Αυτό είναι, ειλικρινά, ένα από τα πιο βολικά features που φέρνει ο App Router.
Ο Ρόλος του default.js
Ένα από τα πιο κρίσιμα αρχεία στα Parallel Routes — και αυτό που θα σας γλιτώσει αρκετούς πονοκεφάλους — είναι το default.tsx. Ο ρόλος του είναι να παρέχει ένα fallback rendering όταν το Next.js δεν μπορεί να ανακτήσει την ενεργή κατάσταση ενός slot.
Πότε συμβαίνει αυτό; Κυρίως κατά το hard navigation (πλήρης ανανέωση σελίδας μέσω browser refresh). Όταν ο χρήστης κάνει refresh, το Next.js χάνει τις πληροφορίες για το ποιο sub-page ήταν ενεργό σε κάθε slot και ψάχνει το default.tsx. Αν δεν βρει, εμφανίζεται 404 error.
Σημαντικό: Ακόμα και το children (implicit slot) μπορεί να χρειαστεί default.tsx σε ορισμένες περιπτώσεις.
// app/@analytics/default.tsx
// Επιστρέφει null ως fallback — δεν εμφανίζει τίποτα
export default function Default() {
return null;
}
Κανόνας αντίχειρα: Προσθέτετε πάντα default.tsx σε κάθε slot. Είναι η ασφαλέστερη πρακτική — τρεις γραμμές κώδικα που σας γλιτώνουν ώρες debugging.
Soft Navigation vs Hard Navigation
Η κατανόηση της διαφοράς μεταξύ soft και hard navigation είναι θεμελιώδης για να δουλέψετε σωστά με Parallel Routes:
- Soft Navigation (Client-side): Η πλοήγηση γίνεται μέσω
<Link>ήrouter.push(). Το Next.js διατηρεί την κατάσταση όλων των slots, ακόμα και αν μόνο ένα αλλάζει. Αυτή είναι η ιδανική συμπεριφορά. - Hard Navigation (Full page reload): Ο χρήστης κάνει refresh ή πληκτρολογεί απευθείας το URL. Το Next.js χάνει την client-side κατάσταση και πέφτει πίσω στο
default.tsxγια κάθε slot που δεν μπορεί να αντιστοιχιστεί στο τρέχον URL.
Πρακτικά Παραδείγματα Parallel Routes
Dashboard με Πολλαπλά Panels
Ας φτιάξουμε ένα ολοκληρωμένο dashboard με τρία ανεξάρτητα panels: στατιστικά, γραφήματα, και πρόσφατη δραστηριότητα. Αυτό είναι ίσως το πιο κλασικό use case για Parallel Routes.
app/dashboard/
├── layout.tsx
├── page.tsx
├── @stats/
│ ├── page.tsx
│ ├── loading.tsx
│ ├── error.tsx
│ └── default.tsx
├── @charts/
│ ├── page.tsx
│ ├── loading.tsx
│ ├── error.tsx
│ └── default.tsx
└── @activity/
├── page.tsx
├── loading.tsx
├── error.tsx
└── default.tsx
// app/dashboard/layout.tsx
export default function DashboardLayout({
children,
stats,
charts,
activity,
}: {
children: React.ReactNode;
stats: React.ReactNode;
charts: React.ReactNode;
activity: React.ReactNode;
}) {
return (
<div className="p-6">
<h1 className="text-3xl font-bold mb-6">Dashboard</h1>
{children}
{/* Πάνω σειρά: Στατιστικά */}
<div className="mb-6">{stats}</div>
{/* Κάτω σειρά: Γραφήματα και Δραστηριότητα */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div>{charts}</div>
<div>{activity}</div>
</div>
</div>
);
}
// app/dashboard/@stats/page.tsx
// Κάθε slot μπορεί να φέρνει τα δικά του δεδομένα ανεξάρτητα
async function getStats() {
const res = await fetch("https://api.example.com/stats", {
next: { revalidate: 60 },
});
return res.json();
}
export default async function StatsPanel() {
const stats = await getStats();
return (
<div className="grid grid-cols-3 gap-4">
<div className="bg-white rounded-lg p-4 shadow">
<p className="text-gray-500 text-sm">Συνολικοί Χρήστες</p>
<p className="text-2xl font-bold">{stats.totalUsers}</p>
</div>
<div className="bg-white rounded-lg p-4 shadow">
<p className="text-gray-500 text-sm">Ενεργές Συνεδρίες</p>
<p className="text-2xl font-bold">{stats.activeSessions}</p>
</div>
<div className="bg-white rounded-lg p-4 shadow">
<p className="text-gray-500 text-sm">Έσοδα Σήμερα</p>
<p className="text-2xl font-bold">{stats.revenue}€</p>
</div>
</div>
);
}
// app/dashboard/@stats/loading.tsx
// Ανεξάρτητο loading state — δεν μπλοκάρει τα υπόλοιπα panels
export default function StatsLoading() {
return (
<div className="grid grid-cols-3 gap-4">
{[1, 2, 3].map((i) => (
<div key={i} className="bg-gray-200 animate-pulse rounded-lg p-4 h-20" />
))}
</div>
);
}
// app/dashboard/@stats/error.tsx
"use client";
// Ανεξάρτητο error boundary — αν σπάσει ένα panel, τα υπόλοιπα λειτουργούν
export default function StatsError({
error,
reset,
}: {
error: Error;
reset: () => void;
}) {
return (
<div className="bg-red-50 rounded-lg p-4">
<p className="text-red-600">Σφάλμα φόρτωσης στατιστικών</p>
<button
onClick={reset}
className="mt-2 text-sm bg-red-600 text-white px-3 py-1 rounded"
>
Δοκιμάστε ξανά
</button>
</div>
);
}
Το μεγάλο πλεονέκτημα εδώ είναι ότι κάθε panel φορτώνεται, εμφανίζεται, και χειρίζεται σφάλματα ανεξάρτητα. Δηλαδή, αν τα στατιστικά αργήσουν (ή ακόμα χειρότερα, σκάσουν), τα γραφήματα και η δραστηριότητα εμφανίζονται κανονικά. Πολύ καλύτερο από ένα spinner που μπλοκάρει τα πάντα.
Conditional Routes Βάσει Ρόλου Χρήστη
Τα Parallel Routes μπορούν να χρησιμοποιηθούν και για conditional rendering βάσει κάποιας λογικής — π.χ. ο ρόλος του χρήστη:
// app/dashboard/layout.tsx
import { checkUserRole } from "@/lib/auth";
export default async function DashboardLayout({
children,
admin,
user,
}: {
children: React.ReactNode;
admin: React.ReactNode;
user: React.ReactNode;
}) {
const role = await checkUserRole();
return (
<div className="p-6">
{children}
{/* Εμφάνιση διαφορετικού περιεχομένου ανάλογα με τον ρόλο */}
{role === "admin" ? admin : user}
</div>
);
}
Έτσι, οι admins βλέπουν πλήρη στατιστικά και εργαλεία διαχείρισης, ενώ οι απλοί χρήστες βλέπουν μόνο τα δικά τους δεδομένα. Χωρίς client-side conditionals, χωρίς useEffect hooks, χωρίς περιττή πολυπλοκότητα.
Tab Groups μέσα σε Slots
Μπορείτε να προσθέσετε ένα nested layout μέσα σε ένα slot για tab navigation. Αυτό είναι κάτι που πολλοί δεν αξιοποιούν αρκετά:
app/dashboard/@analytics/
├── layout.tsx
├── page.tsx
├── visitors/
│ └── page.tsx
├── revenue/
│ └── page.tsx
└── default.tsx
// app/dashboard/@analytics/layout.tsx
import Link from "next/link";
export default function AnalyticsLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="bg-white rounded-lg shadow">
<nav className="flex border-b">
<Link
href="/dashboard"
className="px-4 py-2 text-sm font-medium hover:bg-gray-50"
>
Επισκόπηση
</Link>
<Link
href="/dashboard/visitors"
className="px-4 py-2 text-sm font-medium hover:bg-gray-50"
>
Επισκέπτες
</Link>
<Link
href="/dashboard/revenue"
className="px-4 py-2 text-sm font-medium hover:bg-gray-50"
>
Έσοδα
</Link>
</nav>
<div className="p-4">{children}</div>
</div>
);
}
Το αποτέλεσμα: μόνο το @analytics slot αλλάζει περιεχόμενο κατά την πλοήγηση στα tabs, ενώ τα υπόλοιπα panels παραμένουν στη θέση τους αμετάβλητα.
Τι Είναι τα Intercepting Routes
Τα Intercepting Routes σας επιτρέπουν να φορτώσετε ένα route μέσα στο τρέχον layout, χωρίς πλήρη αλλαγή σελίδας. Σκεφτείτε το κλασικό σενάριο: ο χρήστης κάνει κλικ σε μια φωτογραφία στο feed και αντί να τον πάει σε νέα σελίδα, η φωτογραφία ανοίγει σε modal πάνω από το feed. Ακριβώς όπως το Instagram ή το X (πρώην Twitter).
Τα Intercepting Routes χρησιμοποιούν ειδικές συμβάσεις ονομασίας φακέλων που βασίζονται σε route segments (όχι στη δομή του file system):
(.)— Ίδιο επίπεδο (same level segment)(..)— Ένα επίπεδο πάνω (one level up)(..)(..)— Δύο επίπεδα πάνω (two levels up)(...)— Από τον rootappκατάλογο
Προσοχή εδώ: Η αντιστοίχιση γίνεται βάσει route segments, όχι βάσει file system paths. Αυτό σημαίνει πως οι φάκελοι @slot και τα route groups (group) δεν μετρούν ως segments. Αυτό μπερδεύει αρκετούς developers στην αρχή.
Οι Συμβάσεις Interception
Ας δούμε αναλυτικά κάθε σύμβαση:
(.)— Ίδιο επίπεδο: Χρησιμοποιείται όταν θέλετε να κάνετε intercept ένα route στο ίδιο επίπεδο. Π.χ., από το/feedintercept το/feed/photo.(..)— Ένα επίπεδο πάνω: Για intercept route ένα segment πάνω. Π.χ., από/feed/@modalintercept το/photo(το@modalδεν μετράει ως segment).(..)(..)— Δύο επίπεδα πάνω: Για routes δύο segments πάνω στην ιεραρχία.(...)— Root: Intercept από οπουδήποτε προς ένα route στο rootappdirectory.
Κατά το soft navigation (κλικ σε Link), το intercepting route εμφανίζει το δικό του περιεχόμενο (π.χ. modal). Κατά το hard navigation (refresh ή direct URL), εμφανίζεται η κανονική σελίδα — χωρίς interception. Αυτή η dual συμπεριφορά είναι και η μαγεία τους.
Συνδυασμός Parallel + Intercepting Routes: Δημιουργία Modals
Εδώ τα πράγματα γίνονται πραγματικά ενδιαφέροντα. Ο πιο δημοφιλής συνδυασμός Parallel και Intercepting Routes είναι η δημιουργία modals με URL support. Ας το φτιάξουμε βήμα-βήμα.
Βήμα 1: Δημιουργία της Κύριας Σελίδας
Πρώτα, δημιουργούμε τη σελίδα που θα εμφανίζεται κατά hard navigation (δηλαδή αν κάποιος ανοίξει απευθείας το URL):
// app/photo/[id]/page.tsx
// Αυτή η σελίδα εμφανίζεται κατά hard navigation ή direct URL
import Image from "next/image";
async function getPhoto(id: string) {
const res = await fetch(`https://api.example.com/photos/${id}`);
return res.json();
}
export default async function PhotoPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
const photo = await getPhoto(id);
return (
<div className="max-w-4xl mx-auto p-8">
<h1 className="text-2xl font-bold mb-4">{photo.title}</h1>
<Image
src={photo.url}
alt={photo.title}
width={800}
height={600}
className="rounded-lg"
/>
<p className="mt-4 text-gray-600">{photo.description}</p>
</div>
);
}
Βήμα 2: Δημιουργία του @modal Slot
Τώρα φτιάχνουμε ένα @modal slot στο root level, μαζί με ένα intercepting route:
app/
├── layout.tsx
├── page.tsx
├── photo/
│ └── [id]/
│ └── page.tsx ← Πλήρης σελίδα (hard navigation)
├── @modal/
│ ├── default.tsx ← Fallback (επιστρέφει null)
│ └── (.)photo/
│ └── [id]/
│ └── page.tsx ← Modal version (soft navigation)
└── default.tsx
// app/@modal/default.tsx
// Όταν δεν υπάρχει ενεργό modal, δεν εμφανίζεται τίποτα
export default function ModalDefault() {
return null;
}
// app/@modal/(.)photo/[id]/page.tsx
// Εμφανίζεται ως modal κατά soft navigation
import Modal from "@/components/Modal";
import Image from "next/image";
async function getPhoto(id: string) {
const res = await fetch(`https://api.example.com/photos/${id}`);
return res.json();
}
export default async function PhotoModal({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
const photo = await getPhoto(id);
return (
<Modal>
<div className="p-4">
<h2 className="text-xl font-bold mb-2">{photo.title}</h2>
<Image
src={photo.url}
alt={photo.title}
width={600}
height={400}
className="rounded-lg"
/>
<p className="mt-2 text-gray-600">{photo.description}</p>
</div>
</Modal>
);
}
Βήμα 3: Το Layout Component
Το root layout δέχεται και τα δύο — children και modal — και τα εμφανίζει ταυτόχρονα:
// app/layout.tsx
export default function RootLayout({
children,
modal,
}: {
children: React.ReactNode;
modal: React.ReactNode;
}) {
return (
<html lang="el">
<body>
{children}
{/* Το modal εμφανίζεται πάνω από το κύριο περιεχόμενο */}
{modal}
</body>
</html>
);
}
Βήμα 4: Το Modal Component
Δημιουργούμε ένα επαναχρησιμοποιήσιμο Modal component. Αυτό πρέπει να είναι Client Component γιατί χρησιμοποιεί event handlers και hooks:
// components/Modal.tsx
"use client";
import { useRouter } from "next/navigation";
import { useCallback, useEffect, useRef } from "react";
export default function Modal({ children }: { children: React.ReactNode }) {
const router = useRouter();
const overlayRef = useRef<HTMLDivElement>(null);
// Κλείσιμο modal πατώντας Escape
const onKeyDown = useCallback(
(e: KeyboardEvent) => {
if (e.key === "Escape") {
router.back();
}
},
[router]
);
useEffect(() => {
document.addEventListener("keydown", onKeyDown);
return () => document.removeEventListener("keydown", onKeyDown);
}, [onKeyDown]);
// Κλείσιμο modal κάνοντας κλικ στο overlay
const handleOverlayClick = useCallback(
(e: React.MouseEvent) => {
if (e.target === overlayRef.current) {
router.back();
}
},
[router]
);
return (
<div
ref={overlayRef}
className="fixed inset-0 z-50 flex items-center justify-center bg-black/60"
onClick={handleOverlayClick}
>
<div className="relative bg-white rounded-xl shadow-2xl max-w-2xl w-full mx-4 max-h-[90vh] overflow-y-auto">
<button
onClick={() => router.back()}
className="absolute top-3 right-3 text-gray-500 hover:text-gray-800 text-xl"
aria-label="Κλείσιμο"
>
×
</button>
{children}
</div>
</div>
);
}
Βήμα 5: Κλείσιμο του Modal
Υπάρχουν τρεις βασικοί τρόποι κλεισίματος:
router.back(): Πηγαίνει πίσω στο ιστορικό πλοήγησης. Ιδανικό όταν ο χρήστης ήρθε από εσωτερικό link.<Link href="/">: Πλοηγεί σε συγκεκριμένο URL. Χρήσιμο για explicit navigation.- Catch-all route: Ένα
[...catchAll]route μέσα στο@modalπου επιστρέφειnull— εξασφαλίζει ότι το modal κλείνει σε οποιαδήποτε μη αναμενόμενη πλοήγηση.
// app/@modal/[...catchAll]/page.tsx
// Catch-all: κλείνει το modal σε οποιαδήποτε πλοήγηση
export default function CatchAll() {
return null;
}
Προσωπικά προτιμώ να χρησιμοποιώ πάντα τον catch-all τρόπο σε συνδυασμό με router.back(), γιατί καλύπτει edge cases που μπορεί να μην έχετε σκεφτεί.
Πλήρες Παράδειγμα: Gallery με Photo Modal
Ας δούμε τώρα ένα ολοκληρωμένο παράδειγμα — μια gallery εφαρμογή που χρησιμοποιεί τόσο Parallel όσο και Intercepting Routes μαζί.
app/
├── layout.tsx ← Root layout (children + modal)
├── page.tsx ← Αρχική σελίδα με gallery grid
├── default.tsx ← Default για children slot
├── photo/
│ └── [id]/
│ └── page.tsx ← Πλήρης σελίδα φωτογραφίας
├── @modal/
│ ├── default.tsx ← Modal default (null)
│ ├── (.)photo/
│ │ └── [id]/
│ │ └── page.tsx ← Photo modal (intercepted)
│ └── [...catchAll]/
│ └── page.tsx ← Catch-all (null)
└── components/
└── Modal.tsx ← Modal wrapper component
// app/page.tsx
// Αρχική σελίδα — Gallery grid με φωτογραφίες
import Link from "next/link";
import Image from "next/image";
const photos = [
{ id: "1", src: "/photos/1.jpg", title: "Ηλιοβασίλεμα στη Σαντορίνη" },
{ id: "2", src: "/photos/2.jpg", title: "Ακρόπολη Αθηνών" },
{ id: "3", src: "/photos/3.jpg", title: "Ναύπλιο από ψηλά" },
{ id: "4", src: "/photos/4.jpg", title: "Μύκονος Windmills" },
{ id: "5", src: "/photos/5.jpg", title: "Μετέωρα" },
{ id: "6", src: "/photos/6.jpg", title: "Κρήτη — Μπάλος" },
];
export default function GalleryPage() {
return (
<div className="max-w-6xl mx-auto p-8">
<h1 className="text-3xl font-bold mb-8">Φωτογραφίες Ελλάδας</h1>
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
{photos.map((photo) => (
<Link
key={photo.id}
href={`/photo/${photo.id}`}
className="group relative aspect-square overflow-hidden rounded-lg"
>
<Image
src={photo.src}
alt={photo.title}
fill
className="object-cover transition-transform group-hover:scale-105"
sizes="(max-width: 768px) 50vw, 33vw"
/>
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/30 transition-colors flex items-end">
<p className="text-white p-3 opacity-0 group-hover:opacity-100 transition-opacity">
{photo.title}
</p>
</div>
</Link>
))}
</div>
</div>
);
}
Πώς λειτουργεί η ροή:
- Ο χρήστης βρίσκεται στο
/(gallery grid). - Κάνει κλικ σε μια φωτογραφία — Soft navigation: Το URL αλλάζει σε
/photo/1, αλλά αντί για πλήρη σελίδα, το intercepting route εμφανίζει τη φωτογραφία σε modal πάνω από το gallery. - Μπορεί να κλείσει το modal (επιστροφή στο
/) ή να κάνει refresh. - Αν κάνει refresh στο
/photo/1, εμφανίζεται η πλήρης σελίδα φωτογραφίας — χωρίς modal. - Αν στείλει το URL σε κάποιον φίλο, εκείνος βλέπει την πλήρη σελίδα (hard navigation). Δηλαδή, κάθε φωτογραφία έχει shareable URL!
Συνήθη Προβλήματα και Λύσεις
Μετά από αρκετές ώρες debugging (ναι, αρκετές), ακολουθούν τα πιο κοινά προβλήματα που θα αντιμετωπίσετε.
Λείπει το default.tsx — Εμφανίζεται 404
Πρόβλημα: Μετά από refresh ή hard navigation, εμφανίζεται σφάλμα 404.
Λύση: Προσθέστε default.tsx σε κάθε slot. Αυτό είναι μακράν το πιο συνηθισμένο πρόβλημα:
// app/@modal/default.tsx
// Πάντα προσθέτετε default.tsx σε κάθε slot
export default function Default() {
return null;
}
Το Modal Δεν Κλείνει Μετά την Πλοήγηση
Πρόβλημα: Ο χρήστης πλοηγείται σε άλλη σελίδα αλλά το modal παραμένει ορατό. Αρκετά ενοχλητικό.
Λύση: Προσθέστε ένα catch-all route μέσα στο @modal slot:
// app/@modal/[...catchAll]/page.tsx
export default function CatchAllModal() {
return null;
}
Hard Refresh Σπάει τα Slots
Πρόβλημα: Μετά από hard refresh, μόνο ένα slot εμφανίζεται σωστά ενώ τα υπόλοιπα δείχνουν λάθος περιεχόμενο ή τίποτα.
Λύση: Ελέγξτε τα εξής:
- Κάθε slot έχει
default.tsxγια κάθε route segment. - Τα
default.tsxαρχεία υπάρχουν σε όλα τα nested levels, όχι μόνο στο root level του slot. - Η δομή route segments μέσα σε κάθε slot αντιστοιχεί στη δομή του URL.
app/dashboard/
├── @stats/
│ ├── default.tsx ← Root level default
│ ├── page.tsx
│ └── details/
│ ├── default.tsx ← Nested level default
│ └── page.tsx
Πολλαπλά Modals Εμφανίζονται Ταυτόχρονα
Πρόβλημα: Ανοίγοντας ένα νέο modal, το προηγούμενο δεν κλείνει.
Λύση: Χρησιμοποιήστε ένα μοναδικό @modal slot για όλα τα modals, αντί για πολλαπλά slots:
app/
├── @modal/
│ ├── default.tsx
│ ├── (.)photo/[id]/page.tsx ← Photo modal
│ ├── (.)settings/page.tsx ← Settings modal
│ ├── (.)confirm-delete/page.tsx ← Confirmation modal
│ └── [...catchAll]/page.tsx
Best Practices
Αφού δουλέψετε λίγο με Parallel και Intercepting Routes, θα διαπιστώσετε κάποια patterns που επαναλαμβάνονται. Ακολουθούν οι βέλτιστες πρακτικές που θα σας γλιτώσουν χρόνο:
- Ομαδοποιήστε τα modals σε ένα
@modalslot: Αποφύγετε πολλαπλά modal slots. Ένα@modalμε πολλαπλά intercepting routes μέσα είναι πολύ πιο διαχειρίσιμο. - Πάντα
default.tsxσε κάθε slot: Δεν θα κουραστώ να το επαναλαμβάνω — αυτό αποτρέπει 404 errors κατά hard navigation. Ακόμα και αν νομίζετε ότι δεν χρειάζεται, βάλτε το. - Χρησιμοποιήστε catch-all routes: Ένα
[...catchAll]/page.tsxμέσα σε κάθε modal slot εξασφαλίζει cleanup κατά πλοήγηση σε μη αναμενόμενα routes. - Modal content ως Server Components: Μόνο το wrapper Modal component χρειάζεται να είναι Client Component. Το περιεχόμενο μπορεί να παραμείνει Server Component για καλύτερη απόδοση.
- Δοκιμάστε τόσο soft όσο και hard navigation: Ελέγξτε τη συμπεριφορά με κλικ σε Links (soft) αλλά και με refresh / direct URL (hard). Πολλά bugs εμφανίζονται μόνο στο ένα σενάριο.
- Next.js 16 — Σημείωση: Στο Next.js 16, το
middleware.tsμετονομάστηκε σεproxy.ts. Τα Parallel και Intercepting Routes λειτουργούν κανονικά με τον νέο proxy layer.
Συχνές Ερωτήσεις (FAQ)
Ποια είναι η διαφορά μεταξύ Parallel Routes και Intercepting Routes;
Τα Parallel Routes εμφανίζουν πολλαπλά route segments ταυτόχρονα στο ίδιο layout (π.χ. dashboard panels). Τα Intercepting Routes "παρεμβάλλουν" τη φόρτωση ενός route και το εμφανίζουν στο τρέχον context (π.χ. modal πάνω από feed). Συχνά χρησιμοποιούνται μαζί, αλλά μπορούν να δουλέψουν και ανεξάρτητα.
Χρειάζομαι πάντα default.tsx σε κάθε slot;
Τεχνικά, χρειάζεται μόνο όταν υπάρχει πιθανότητα hard navigation σε URL που δεν αντιστοιχεί στο slot. Στην πράξη; Βάζετε πάντα. Τρεις γραμμές κώδικα σας γλιτώνουν ώρες debugging δύσκολα εντοπίσιμων 404 σφαλμάτων.
Μπορώ να χρησιμοποιήσω Parallel Routes χωρίς Intercepting Routes;
Φυσικά. Τα Parallel Routes είναι εξαιρετικά χρήσιμα μόνα τους — dashboards, split views, conditional layouts, tab groups. Τα Intercepting Routes χρειάζονται μόνο αν θέλετε να "παρεμβάλετε" τη φόρτωση ενός URL (π.χ. για εμφάνιση σε modal).
Πώς λειτουργούν τα Parallel Routes με το Streaming και το Suspense;
Κάθε slot κάνει stream ανεξάρτητα. Προσθέτετε loading.tsx σε κάθε slot, και κάθε ένα εμφανίζει το δικό του loading state χωρίς να μπλοκάρει τα υπόλοιπα. Αυτό γίνεται αυτόματα μέσω React.Suspense που χρησιμοποιεί εσωτερικά το Next.js.
Τα Parallel Routes λειτουργούν στο Next.js 16;
Ναι, τα Parallel και Intercepting Routes λειτουργούν πλήρως στο Next.js 16. Η API παραμένει σταθερή. Απλά βεβαιωθείτε ότι χρησιμοποιείτε τη σύνταξη params: Promise<{ id: string }> με await για dynamic route params — αυτή είναι υποχρεωτική από το Next.js 15 και μετά.