Parallel Routes (@slots)

Render multiple independent pages into named slots of one layout, each with its own loading, error, and navigation state.

Once you have built a few dashboards you hit the same wall: you want two or three regions on screen that each fetch their own data, stream independently, and even navigate on their own. Stuffing that into a single page.tsx gets ugly fast. Parallel routes are the elegant answer.

The mental model

A folder named with an @ prefix is a slot. It does not add a URL segment. Instead, the slot is handed to the sibling layout.tsx as a prop whose name matches the folder. So @team and @analytics become props team and analytics alongside the usual children.

app/
  dashboard/
    layout.tsx        // receives { children, team, analytics }
    page.tsx          // fills the implicit "children" slot
    @team/
      page.tsx
    @analytics/
      page.tsx

Each slot is a full sub-tree: it can have its own loading.tsx, error.tsx, and nested routes. They render at the same time and stream independently, so a slow analytics query never blocks the team list.

The one that trips everyone up: default.tsx

Slots keep their own navigation state. On a hard navigation (full reload) to a URL a slot cannot match, Next needs to know what to render there. It looks for default.tsx in the slot. If it is missing, you get a 404 for the whole route. This is the single most common parallel-routes bug, so add a default.tsx to every slot early.

Example

Example · typescript
// app/dashboard/layout.tsx
// Slots arrive as props named after their @folder.
export default function DashboardLayout({
  children,
  team,
  analytics,
}: {
  children: React.ReactNode;
  team: React.ReactNode;
  analytics: React.ReactNode;
}) {
  return (
    <section>
      <header>{children}</header>
      <div className="grid">
        {/* Each slot streams on its own timeline */}
        {team}
        {analytics}
      </div>
    </section>
  );
}

// app/dashboard/@analytics/default.tsx
// Required: what to show after a hard nav when the slot
// has no matching sub-route. Missing this = surprise 404.
export default function Default() {
  return null;
}

When to use it

  • A dashboard renders an @analytics slot and a @team slot independently so each can have its own loading state.
  • A social app shows a notifications panel in @panel while the main feed updates in @feed without either blocking the other.
  • A multi-tab interface uses parallel routes so the active tab can stream its data while sibling tabs remain mounted.

More examples

Slot folder structure

Folder names prefixed with @ become named slot props that the parent layout receives alongside children.

Example · bash
app/
└── dashboard/
    ├── layout.tsx          # receives @analytics and @team
    ├── @analytics/
    │   └── page.tsx         # /dashboard (analytics slot)
    └── @team/
        └── page.tsx         # /dashboard (team slot)

Layout consuming two slots

Each slot is a prop, so the layout positions them independently while they load and error separately.

Example · ts
// app/dashboard/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">
      <div>{analytics}</div>
      <div>{team}</div>
      <div className="col-span-2">{children}</div>
    </div>
  );
}

Default.tsx for unmatched slots

default.tsx renders when a slot has no matching page for the current URL, preventing a 404 for sibling navigation.

Example · ts
// app/dashboard/@analytics/default.tsx
export default function AnalyticsDefault() {
  return <p>No analytics selected.</p>;
}

Discussion

  • Be the first to comment on this lesson.