Next.js course Β· Module 2: Routing and Layouts
Advanced Routing in Next.js 16 - Parallel Routes, Intercepting Routes, and Route Groups
In this lesson6
In the Quantum Metropolis, the advanced transport system allows simultaneous travel along multiple parallel paths, in-flight traffic interception and redirection, and logical route grouping without affecting their addresses. Similarly, Next.js 16 offers advanced routing techniques that enable creating complex user interfaces with elegance and efficiency.
In this chapter, you will learn three powerful routing patterns in the App Router:
- Parallel Routes - rendering multiple routes simultaneously
- Intercepting Routes - intercepting navigation and showing alternative views
- Route Groups - organizing routes without affecting the URL
Parallel Routes - Simultaneous Route Rendering
What Are Parallel Routes?
Parallel Routes allow simultaneous rendering of multiple pages (or parts of pages) in the same layout. It's like holographic displays in the Quantum Metropolis that can show multiple independent information streams simultaneously.
Naming Convention - @folder
Parallel Routes use a special naming convention with the @ prefix:
app/
βββ layout.tsx
βββ page.tsx
βββ @analytics/ # Slot "analytics"
β βββ default.tsx
β βββ page.tsx
βββ @team/ # Slot "team"
β βββ default.tsx
β βββ page.tsx
βββ @activity/ # "activity" Slot
βββ default.tsx
βββ page.tsxBasic Example - Dashboard with Multiple Panels
Imagine a Quantum Metropolis control panel that simultaneously displays analytics, team activity, and recent events:
// app/dashboard/layout.tsx
export default function DashboardLayout({
children,
analytics,
team,
activity,
}: {
children: React.ReactNode;
analytics: React.ReactNode;
team: React.ReactNode;
activity: React.ReactNode;
}) {
return (
<div className="quantum-dashboard">
<div className="main-content">
{children}
</div>
<div className="grid grid-cols-3 gap-6 mt-6">
<div className="panel">
<h2>Quantum Analytics</h2>
{analytics}
</div>
<div className="panel">
<h2>Team</h2>
{team}
</div>
<div className="panel">
<h2>Recent Activity</h2>
{activity}
</div>
</div>
</div>
);
}// app/dashboard/@analytics/page.tsx
async function getAnalytics() {
const res = await fetch('https://api.quantum.com/analytics', {
next: { revalidate: 60 }
});
return res.json();
}
export default async function Analytics() {
const data = await getAnalytics();
return (
<div className="analytics-panel">
<div className="stat">
<span className="label">Quantum Energy</span>
<span className="value">{data.energy}%</span>
</div>
<div className="stat">
<span className="label">System Efficiency</span>
<span className="value">{data.efficiency}%</span>
</div>
</div>
);
}// app/dashboard/@team/page.tsx
async function getTeam() {
const res = await fetch('https://api.quantum.com/team', {
next: { revalidate: 300 }
});
return res.json();
}
export default async function Team() {
const members = await getTeam();
return (
<ul className="team-list">
{members.map(member => (
<li key={member.id} className="team-member">
<img src={member.avatar} alt={member.name} />
<span>{member.name}</span>
<span className="status">{member.status}</span>
</li>
))}
</ul>
);
}// app/dashboard/@activity/page.tsx
async function getActivity() {
const res = await fetch('https://api.quantum.com/activity', {
cache: 'no-store' // Always fresh data
});
return res.json();
}
export default async function Activity() {
const activities = await getActivity();
return (
<div className="activity-feed">
{activities.map(activity => (
<div key={activity.id} className="activity-item">
<span className="timestamp">{activity.time}</span>
<p>{activity.description}</p>
</div>
))}
</div>
);
}Advantages of Parallel Routes
- Independent loading - each slot can be loaded independently with its own loading state
- Independent errors - an error in one slot doesn't affect others
- Selective navigation - you can navigate within one slot without affecting others
- Conditional rendering - you can conditionally show/hide slots
Default.tsx - Handling Missing Slots
When a user navigates in a way that doesn't match the slot structure (for example, reloads the page at an address the slot does not handle), Next.js uses the default.tsx file. In Next.js 16 this file is required in every slot, because without it next build fails:
// app/dashboard/@analytics/default.tsx
export default function DefaultAnalytics() {
return <div>No analytics data available</div>;
}Conditional Slot Rendering
// app/dashboard/layout.tsx
import { getCurrentUser } from '@/lib/auth';
export default async function DashboardLayout({
children,
analytics,
team,
activity,
}: {
children: React.ReactNode;
analytics: React.ReactNode;
team: React.ReactNode;
activity: React.ReactNode;
}) {
// The role comes from the session: params only holds the dynamic URL segments
const user = await getCurrentUser();
const isAdmin = user?.role === 'admin';
return (
<div className="quantum-dashboard">
<div className="main-content">{children}</div>
<div className="panels">
{analytics}
{team}
{isAdmin && activity} {/* Only for admins */}
</div>
</div>
);
}Intercepting Routes - Navigation Interception
What Are Intercepting Routes?
Intercepting Routes allow intercepting navigation and displaying an alternative view while the URL changes. It's like quantum tunnels in the Quantum Metropolis - they enable shortcuts without changing the final destination.
Naming Convention
Intercepting Routes use special prefixes:
(.)- intercepts segments at the same level(..)- intercepts segments one level up(..)(..)- intercepts segments two levels up(...)- intercepts segments from theapp/root
Example 1: Image Modal
A classic use case - a photo gallery where clicking opens a modal but the URL changes:
app/
βββ layout.tsx
βββ page.tsx # Gallery /
βββ photos/
β βββ [id]/
β βββ page.tsx # Full photo page /photos/123
βββ @modal/
βββ default.tsx # Returns null while the modal is closed (required in Next.js 16)
βββ (.)photos/
βββ [id]/
βββ page.tsx # Intercepting modal /photos/123@modal is a slot, not a URL segment, so photos from app/photos are intercepted by (.)photos. Writing (..)photos at this level makes next build fail with βCannot use (..) marker at the root level, use (.) insteadβ.
// app/page.tsx - Gallery
import Link from 'next/link';
import Image from 'next/image';
const photos = [
{ id: '1', url: '/photos/quantum-city-1.jpg', title: 'Quantum District' },
{ id: '2', url: '/photos/quantum-city-2.jpg', title: 'Central Plaza' },
{ id: '3', url: '/photos/quantum-city-3.jpg', title: 'Night Skyline' },
];
export default function Gallery() {
return (
<div className="gallery-grid">
{photos.map(photo => (
<Link key={photo.id} href={`/photos/${photo.id}`}>
<div className="photo-card">
<Image
src={photo.url}
alt={photo.title}
width={300}
height={200}
/>
<p>{photo.title}</p>
</div>
</Link>
))}
</div>
);
}// app/photos/[id]/page.tsx - Full photo page
import Image from 'next/image';
async function getPhoto(id: string) {
// Fetch photo data
return {
id,
url: `/photos/quantum-city-${id}.jpg`,
title: 'Quantum City',
description: 'View of Quantum Metropolis',
};
}
export default async function PhotoPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const photo = await getPhoto((await params).id);
return (
<div className="photo-full-page">
<h1>{photo.title}</h1>
<Image
src={photo.url}
alt={photo.title}
width={1200}
height={800}
/>
<p>{photo.description}</p>
</div>
);
}// app/@modal/(.)photos/[id]/page.tsx - Modal
'use client';
import { useRouter } from 'next/navigation';
import Image from 'next/image';
import { use, useEffect, useState } from 'react';
export default function PhotoModal({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = use(params);
const router = useRouter();
const [photo, setPhoto] = useState(null);
useEffect(() => {
// Fetch photo data
fetch(`/api/photos/${id}`)
.then(res => res.json())
.then(setPhoto);
}, [id]);
if (!photo) return null;
return (
<div
className="modal-overlay"
onClick={() => router.back()}
>
<div
className="modal-content"
onClick={(e) => e.stopPropagation()}
>
<button
className="modal-close"
onClick={() => router.back()}
aria-label="Close"
>
Γ
</button>
<Image
src={photo.url}
alt={photo.title}
width={800}
height={600}
/>
<h2>{photo.title}</h2>
<p>{photo.description}</p>
</div>
</div>
);
}// app/layout.tsx - Layout with modal
export default function RootLayout({
children,
modal,
}: {
children: React.ReactNode;
modal: React.ReactNode;
}) {
return (
<html lang="en">
<body>
{children}
{modal}
</body>
</html>
);
}The @modal slot also needs a default.tsx file that renders nothing while the modal is closed:
// app/@modal/default.tsx
export default function Default() {
return null;
}Example 2: Login Modal with Intercepting Route
app/
βββ layout.tsx
βββ page.tsx
βββ login/
β βββ page.tsx # Full login page /login
βββ @auth/
βββ default.tsx # Returns null while the modal is closed
βββ (.)login/
βββ page.tsx # Login modal// app/@auth/(.)login/page.tsx
'use client';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
export default function LoginModal() {
const router = useRouter();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
// Login logic
await loginUser(email, password);
router.back();
};
return (
<div className="modal-overlay" onClick={() => router.back()}>
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
<h2>Log in to Quantum Metropolis</h2>
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
required
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Password"
required
/>
<button type="submit">Log in</button>
</form>
</div>
</div>
);
}The root layout must also receive the auth prop and render {auth}, just like {modal} in example 1.
Advantages of Intercepting Routes
- Better UX - quick modals without full page reload
- Shareable URLs - the URL changes, so the link can be shared
- Graceful fallback - direct URL access shows the full page
- Context preservation - the background remains visible behind the modal
Route Groups - Route Organization
What Are Route Groups?
Route Groups allow logical grouping of routes without affecting the URL structure. It's like districts in the Quantum Metropolis - they organize the city but don't affect building addresses.
Naming Convention - (folder)
Route Groups use parentheses in folder names:
app/
βββ (marketing)/ # Group - doesn't affect the URL
β βββ layout.tsx # Marketing layout
β βββ about/
β β βββ page.tsx # URL: /about
β βββ contact/
β βββ page.tsx # URL: /contact
βββ (shop)/ # Group - doesn't affect the URL
β βββ layout.tsx # Shop layout
β βββ products/
β β βββ page.tsx # URL: /products
β βββ cart/
β βββ page.tsx # URL: /cart
βββ (dashboard)/ # Group - doesn't affect the URL
βββ layout.tsx # Dashboard layout
βββ analytics/
β βββ page.tsx # URL: /analytics
βββ settings/
βββ page.tsx # URL: /settingsExample 1: Different Layouts for Different Sections
// app/(marketing)/layout.tsx
import MarketingHeader from '@/components/MarketingHeader';
import MarketingFooter from '@/components/MarketingFooter';
export default function MarketingLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="marketing-layout">
<MarketingHeader />
<main className="marketing-content">
{children}
</main>
<MarketingFooter />
</div>
);
}// app/(dashboard)/layout.tsx
import DashboardSidebar from '@/components/DashboardSidebar';
import DashboardHeader from '@/components/DashboardHeader';
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="dashboard-layout">
<DashboardHeader />
<div className="dashboard-container">
<DashboardSidebar />
<main className="dashboard-content">
{children}
</main>
</div>
</div>
);
}Example 2: Organization by Functionality
app/
βββ (auth)/
β βββ login/
β β βββ page.tsx # /login
β βββ register/
β β βββ page.tsx # /register
β βββ forgot-password/
β βββ page.tsx # /forgot-password
βββ (public)/
β βββ about/
β β βββ page.tsx # /about
β βββ blog/
β βββ [slug]/
β βββ page.tsx # /blog/:slug
βββ (protected)/
βββ layout.tsx # Shared group layout
βββ dashboard/
β βββ page.tsx # /dashboard
βββ profile/
βββ page.tsx # /profileNote that the (protected) group has no proxy.ts file: there can be only one proxy (middleware.ts in Next.js 15 and earlier), in the project root. You protect these routes with a matcher in the root proxy.ts and by checking the session close to the data.
Example 3: Multiple Root Layouts
Route Groups allow creating multiple root layouts in the same application:
app/
βββ (shop)/
β βββ layout.tsx # Root layout for the shop
β βββ products/
β βββ page.tsx
βββ (blog)/
βββ layout.tsx # Root layout for the blog
βββ posts/
βββ page.tsx// app/(shop)/layout.tsx
export default function ShopRootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="pl">
<body className="shop-theme">
{children}
</body>
</html>
);
}// app/(blog)/layout.tsx
export default function BlogRootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="pl">
<body className="blog-theme">
{children}
</body>
</html>
);
}Advantages of Route Groups
- Code organization - logical grouping without affecting the URL
- Different layouts - ability to apply different layouts for different sections
- Better project structure - easier management of large applications
- Separation of concerns - different teams can work on different groups
Combining All Three Techniques
The true power of the App Router reveals itself when you combine all three techniques together:
app/
βββ (marketing)/
β βββ layout.tsx
β βββ page.tsx
β βββ @modal/
β βββ default.tsx
β βββ (.)contact/
β βββ page.tsx
βββ (dashboard)/
β βββ layout.tsx
β βββ @analytics/
β β βββ default.tsx
β β βββ page.tsx
β βββ @team/
β β βββ default.tsx
β β βββ page.tsx
β βββ @activity/
β βββ default.tsx
β βββ page.tsx
βββ (shop)/
βββ layout.tsx
βββ products/
β βββ [id]/
β βββ page.tsx
βββ @cart/
βββ default.tsx
βββ (.)products/
βββ [id]/
βββ page.tsxExample: E-commerce with Advanced Routing
// app/(shop)/layout.tsx
export default function ShopLayout({
children,
cart,
}: {
children: React.ReactNode;
cart: React.ReactNode;
}) {
return (
<div className="shop-layout">
<ShopHeader />
<main>{children}</main>
{cart}
<ShopFooter />
</div>
);
}// app/(shop)/products/[id]/page.tsx
async function getProduct(id: string) {
const res = await fetch(`https://api.quantum.com/products/${id}`);
return res.json();
}
export default async function ProductPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const product = await getProduct((await params).id);
return (
<div className="product-page">
<h1>{product.name}</h1>
<p>{product.description}</p>
<button>Add to cart</button>
</div>
);
}// app/(shop)/@cart/(.)products/[id]/page.tsx
'use client';
import { use } from 'react';
import { useRouter } from 'next/navigation';
export default function QuickViewModal({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = use(params);
const router = useRouter();
return (
<div className="modal-overlay" onClick={() => router.back()}>
<div className="quick-view" onClick={(e) => e.stopPropagation()}>
<ProductQuickView productId={id} />
<button onClick={() => router.back()}>Close</button>
</div>
</div>
);
}Both modals are Client Components, so they cannot use await. Since Next.js 15 params is a Promise, and Next.js 16 no longer lets you read it synchronously, which is why we unwrap it with React's use hook.
Best Practices
1. Use Parallel Routes for Independent Sections
// Good - independent panels
app/dashboard/
βββ @analytics/
βββ @team/
βββ @notifications/
// Bad - dependent components
app/dashboard/
βββ @header/ // This should be a component, not a slot
βββ @footer/ // This should be a component, not a slot
βββ @content/2. Use Intercepting Routes for Modals and Overlays
// Good
app/
βββ photos/[id]/page.tsx
βββ @modal/(.)photos/[id]/page.tsx
// Bad - modal as a separate route
app/
βββ photos/[id]/page.tsx
βββ modal/photos/[id]/page.tsx3. Use Route Groups for Organization
// Good - logical grouping
app/
βββ (marketing)/
βββ (dashboard)/
βββ (auth)/
// Bad - grouping by file type
app/
βββ (pages)/
βββ (components)/
βββ (api)/Summary
Advanced routing techniques in Next.js 16 are like the advanced transport network of the Quantum Metropolis - they enable creating complex, efficient, and elegant user interfaces:
- Parallel Routes - render multiple routes simultaneously for independent sections
- Intercepting Routes - intercept navigation for better UX with modals and overlays
- Route Groups - organize code without affecting the URL
These techniques give you incredible control over the application structure and user experience. Just as engineers of the Quantum Metropolis design multi-level transport systems for maximum efficiency, you too can design advanced routing structures for optimal UX.
The key to success is understanding when to use each technique and how to combine them to create a cohesive, efficient Next.js 16 application.
Code for this lesson: App.tsx
import React, { useState } from 'react';
// Simulation of app/layout.tsx with the @feed and @stats slots and the intercepting route @modal/(.)photos/[id]
const photos = [
{ id: '1', title: 'Quantum District' },
{ id: '2', title: 'Central Plaza' },
{ id: '3', title: 'Skyline at night' },
];
const panel = { flex: 1, padding: '12px', border: '1px solid #64ffda', borderRadius: '8px' };
const button = { margin: '4px', padding: '6px 10px', background: '#1a1a2e', color: '#64ffda', border: '1px solid #64ffda', borderRadius: '6px', cursor: 'pointer' };
export default function ParallelInterceptingDemo() {
const [url, setUrl] = useState('/');
const [modalId, setModalId] = useState<string | null>(null);
// A Link click (in-app navigation): the route is intercepted by @modal/(.)photos/[id]
const openPhoto = (id: string) => { setUrl('/photos/' + id); setModalId(id); };
// A page refresh (F5): no interception, the full page app/photos/[id]/page.tsx renders
const refresh = () => setModalId(null);
// router.back(): back to the gallery
const back = () => { setUrl('/'); setModalId(null); };
const fullPage = url !== '/' && modalId === null;
const photo = photos.find((p) => '/photos/' + p.id === url);
return (
<div style={{ fontFamily: 'monospace', background: '#0f0f23', color: '#e0e0e0', minHeight: '100vh', padding: '16px' }}>
<p>Address: <strong>{url}</strong></p>
{fullPage && photo ? (
<div style={panel}>
<h2>Full page: app/photos/[id]/page.tsx</h2>
<p>{photo.title}</p>
<button style={button} onClick={back}>Back to the gallery</button>
</div>
) : (
<div style={{ display: 'flex', gap: '12px' }}>
<div style={panel}>
<h3>Slot @feed</h3>
{photos.map((p) => (
<button key={p.id} style={button} onClick={() => openPhoto(p.id)}>{p.title}</button>
))}
</div>
<div style={panel}>
<h3>Slot @stats</h3>
<p>Photos in the gallery: {photos.length}</p>
<p>Both slots render at the same time in the same layout.</p>
</div>
</div>
)}
{modalId && photo && (
<div style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.7)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<div style={{ ...panel, flex: 'none', background: '#1a1a2e', maxWidth: '320px' }}>
<h3>Modal from @modal/(.)photos/[id]</h3>
<p>{photo.title}</p>
<p>The gallery stayed underneath and the address changed to {url}.</p>
<button style={button} onClick={back}>Close (router.back)</button>
<button style={button} onClick={refresh}>Refresh the page (F5)</button>
</div>
</div>
)}
</div>
);
}Docs entries used in this lesson
Spotted a mistake in this lesson?
Check yourself
Answer the questions from this lesson. Pick an answer to see right away whether it is correct.
1. What is the default.tsx file used for in Parallel Routes?
2. Which notation intercepts a route segment one level above?
Hands-on tasks in the game
- Code editor
Build a layout with two parallel slots @main and @sidebar: default-export a DashboardLayout({ main, sidebar }) component that displays both slots side by side in one container with display: flex or grid (e.g. a div with style={{ display: 'flex' }} or the flex class).
- Code editor
Combine several App Router features in one file, app/products/[id]/page.tsx, step by step: 1) export async function generateMetadata({ params }) returns { title: 'Product ' + id }; 2) export default async function ProductPage({ params }) reads id with await params; 3) if id is not made of digits only, call notFound() from next/navigation; 4) otherwise return a main element with an h1 heading βProduct {id}β and a Link component (next/link) that leads to /products.