|$ curl https://forge-ai.dev/api/markdown?path=docs/nextjs/parallel-routes
$cat docs/next.js-—-parallel-routes.md
updated Last week·14 min read·published

Next.js — Parallel Routes

Next.jsAdvanced🎯Free Tools
Introduction

Parallel Routes let you render multiple pages in the same layout simultaneously using named slots. This is useful for complex layouts like dashboards where different sections load independently, or for split views where two routes render side by side.

@slot Syntax
app/dashboard/layout.tsx
TSX
1// Define slots in the layout with @ prefix
2// app/dashboard/layout.tsx
3
4export default function DashboardLayout({
5 children,
6 analytics,
7 team,
8 settings,
9}: {
10 children: React.ReactNode;
11 analytics: React.ReactNode;
12 team: React.ReactNode;
13 settings: React.ReactNode;
14}) {
15 return (
16 <div className="flex min-h-screen">
17 {/* Sidebar navigation */}
18 <nav className="w-64 border-r p-4">
19 <h2 className="font-bold mb-4">Dashboard</h2>
20 <ul className="space-y-2">
21 <li><a href="/dashboard">Overview</a></li>
22 <li><a href="/dashboard/analytics">Analytics</a></li>
23 <li><a href="/dashboard/team">Team</a></li>
24 <li><a href="/dashboard/settings">Settings</a></li>
25 </ul>
26 </nav>
27
28 {/* Named slot for analytics */}
29 <div className="flex-1 p-6">
30 {analytics}
31 </div>
32
33 {/* Named slot for team */}
34 <div className="w-80 border-l p-4">
35 {team}
36 </div>
37 </div>
38 );
39}
40
41// The @analytics, @team, @settings folders create the slots:
42// app/
43// dashboard/
44// layout.tsx
45// page.tsx → children slot
46// analytics/
47// page.tsx → analytics slot
48// team/
49// page.tsx → team slot
50// settings/
51// page.tsx → settings slot

info

Each named slot corresponds to a folder with the same name prefixed by @. The layout receives them as props matching the slot names.
Concurrent Rendering
concurrent.tsx
TSX
1// Each slot renders independently — they don't block each other
2
3// app/dashboard/analytics/page.tsx
4async function getAnalytics() {
5 // This might be slow
6 const data = await fetch("https://api.example.com/analytics", {
7 next: { revalidate: 3600 },
8 });
9 return data.json();
10}
11
12export default async function AnalyticsPage() {
13 const analytics = await getAnalytics();
14
15 return (
16 <div>
17 <h1 className="text-2xl font-bold mb-4">Analytics</h1>
18 <div className="grid grid-cols-2 gap-4">
19 <div className="p-4 border rounded">
20 <p className="text-sm text-gray-500">Page Views</p>
21 <p className="text-3xl font-bold">{analytics.pageViews}</p>
22 </div>
23 <div className="p-4 border rounded">
24 <p className="text-sm text-gray-500">Unique Visitors</p>
25 <p className="text-3xl font-bold">{analytics.visitors}</p>
26 </div>
27 </div>
28 </div>
29 );
30}
31
32// app/dashboard/team/page.tsx
33async function getTeamMembers() {
34 const data = await fetch("https://api.example.com/team");
35 return data.json();
36}
37
38export default async function TeamPage() {
39 const members = await getTeamMembers();
40
41 return (
42 <div>
43 <h2 className="font-bold mb-3">Team Members</h2>
44 <ul className="space-y-2">
45 {members.map((member: any) => (
46 <li key={member.id} className="flex items-center gap-2">
47 <img src={member.avatar} className="w-8 h-8 rounded-full" />
48 <span>{member.name}</span>
49 </li>
50 ))}
51 </ul>
52 </div>
53 );
54}
55
56// Both slots load in parallel — the dashboard layout doesn't wait
57// for one to finish before rendering the other
Loading & Error States
loading_error.tsx
TSX
1// Each slot has its own loading.tsx and error.tsx
2
3// app/dashboard/analytics/loading.tsx
4export default function AnalyticsLoading() {
5 return (
6 <div className="animate-pulse space-y-4">
7 <div className="h-8 w-48 bg-gray-800 rounded" />
8 <div className="grid grid-cols-2 gap-4">
9 <div className="h-24 bg-gray-800 rounded" />
10 <div className="h-24 bg-gray-800 rounded" />
11 </div>
12 </div>
13 );
14}
15
16// app/dashboard/analytics/error.tsx
17"use client";
18
19export default function AnalyticsError({
20 error,
21 reset,
22}: {
23 error: Error;
24 reset: () => void;
25}) {
26 return (
27 <div className="p-4 border border-red-500/30 rounded">
28 <h2 className="text-red-400 font-bold">Analytics Error</h2>
29 <p className="text-sm text-gray-400">{error.message}</p>
30 <button onClick={reset} className="mt-2 text-sm text-red-400 underline">
31 Try again
32 </button>
33 </div>
34 );
35}
36
37// app/dashboard/team/loading.tsx
38export default function TeamLoading() {
39 return (
40 <div className="animate-pulse space-y-3">
41 {[1, 2, 3].map((i) => (
42 <div key={i} className="flex items-center gap-2">
43 <div className="w-8 h-8 bg-gray-800 rounded-full" />
44 <div className="h-4 w-24 bg-gray-800 rounded" />
45 </div>
46 ))}
47 </div>
48 );
49}
50
51// app/dashboard/team/error.tsx
52"use client";
53
54export default function TeamError({
55 error,
56 reset,
57}: {
58 error: Error;
59 reset: () => void;
60}) {
61 return (
62 <div className="p-4 border border-yellow-500/30 rounded">
63 <h2 className="text-yellow-400 font-bold">Team Error</h2>
64 <p className="text-sm text-gray-400">{error.message}</p>
65 <button onClick={reset} className="mt-2 text-sm text-yellow-400 underline">
66 Retry
67 </button>
68 </div>
69 );
70}

info

Each slot can have its own loading.tsx and error.tsx. This means a slow or failing analytics section won't affect the team section.
Use Cases
use_cases.tsx
TSX
1// Use case 1: Dashboard with independent sections
2// app/dashboard/layout.tsx
3export default function Layout({
4 overview,
5 chart,
6 table,
7}: {
8 overview: React.ReactNode;
9 chart: React.ReactNode;
10 table: React.ReactNode;
11}) {
12 return (
13 <div className="grid grid-cols-3 gap-4">
14 <div>{overview}</div>
15 <div>{chart}</div>
16 <div className="col-span-2">{table}</div>
17 </div>
18 );
19}
20
21// Use case 2: Split view (email client)
22// app/mail/layout.tsx
23export default function MailLayout({
24 folder,
25 message,
26}: {
27 folder: React.ReactNode;
28 message: React.ReactNode;
29}) {
30 return (
31 <div className="flex h-screen">
32 <div className="w-64 border-r overflow-auto">{folder}</div>
33 <div className="w-80 border-r overflow-auto">{message}</div>
34 <div className="flex-1">{/* Email content */}</div>
35 </div>
36 );
37}
38
39// Use case 3: Marketing site with shared navigation
40// app/marketing/layout.tsx
41export default function MarketingLayout({
42 header,
43 content,
44 footer,
45}: {
46 header: React.ReactNode;
47 content: React.ReactNode;
48 footer: React.ReactNode;
49}) {
50 return (
51 <div>
52 {header}
53 <main>{content}</main>
54 {footer}
55 </div>
56 );
57}
58
59// Use case 4: Tabs with parallel content
60// app/settings/layout.tsx
61export default function SettingsLayout({
62 profile,
63 billing,
64 security,
65}: {
66 profile: React.ReactNode;
67 billing: React.ReactNode;
68 security: React.ReactNode;
69}) {
70 return (
71 <div>
72 <Tabs>
73 <Tab label="Profile">{profile}</Tab>
74 <Tab label="Billing">{billing}</Tab>
75 <Tab label="Security">{security}</Tab>
76 </Tabs>
77 </div>
78 );
79}
Active Route Indicators
active_routes.tsx
TSX
1// Use useSelectedLayoutSegment to detect active slot
2
3"use client";
4import { useSelectedLayoutSegment } from "next/navigation";
5
6export function NavLink({
7 href,
8 children,
9}: {
10 href: string;
11 children: React.ReactNode;
12}) {
13 const segment = useSelectedLayoutSegment();
14 const isActive = href === "/" + segment;
15
16 return (
17 <a
18 href={href}
19 className={`block px-3 py-2 rounded ${
20 isActive ? "bg-blue-600 text-white" : "text-gray-400 hover:bg-gray-800"
21 }`}
22 >
23 {children}
24 </a>
25 );
26}
27
28// Use in the sidebar
29export function Sidebar() {
30 return (
31 <nav className="space-y-1">
32 <NavLink href="/dashboard">Overview</NavLink>
33 <NavLink href="/dashboard/analytics">Analytics</NavLink>
34 <NavLink href="/dashboard/team">Team</NavLink>
35 <NavLink href="/dashboard/settings">Settings</NavLink>
36 </nav>
37 );
38}
39
40// Conditional slot rendering
41// Hide a slot when no active route matches
42export default function Layout({
43 children,
44 modal,
45}: {
46 children: React.ReactNode;
47 modal: React.ReactNode;
48}) {
49 // modal slot renders null (via default.tsx) when no route matches
50 return (
51 <div>
52 {children}
53 {modal}
54 </div>
55 );
56}
57
58// app/@modal/default.tsx — renders when no route matches the slot
59export default function Default() {
60 return null; // Slot is empty when not matched
61}
$Blueprint — Engineering Documentation·Section ID: NEXT-PRL·Revision: 1.0