Next.js: Rotas Paralelas e Rotas de Interceptação

Última atualização: 2026-08-26

O roteamento paralelo é como vários monitores — cada tela opera de forma independente, mas está interconectada; o roteamento de interceptação é como um ponto de coleta de encomendas — ele intercepta os pacotes no caminho, processa-os e depois continua a entrega.

1. O Que Você Vai Aprender



2. Uma História Real de uma Gerente de Produto

(1) Ponto de Dor: Experiência ruim de pop-up no feed

Alice está desenvolvendo um recurso de navegação de imagens para uma plataforma de conteúdo e encontrou alguns problemas de experiência do usuário:

"Quando um usuário tocava em uma imagem no Feed, exibíamos um modal em tela cheia e a URL mudava para /photos/123. Como resultado, quando o usuário tentava retornar ao Feed pressionando o botão voltar do navegador, ele saía de todo o aplicativo. O usuário reclamou: 'Eu só queria ver uma foto — por que não consigo voltar para a página inicial?'"

Problemas Analisados por Alice:

Problema Impacto Avaliação do Usuário
Modal não pode ser atualizado independentemente Atualizar o modal causa a atualização de toda a página 2/5
Botão voltar sai do aplicativo Interrupção do fluxo do usuário 1,5/5
Barra lateral e conteúdo principal estão dessincronizados Navegação confusa 2,5/5
Visão da Equipe e Dashboard estão separados Baixa Eficiência de Gerenciamento 3/5

(2) A Solução com Rotas Paralelas + Interceptação

Use @modal para exibir uma janela pop-up com roteamento paralelo + (.)photo para interceptar a navegação de imagens no feed.

TEXT 📖 Somente leitura
src/app/
├── layout.tsx              # Layout Principal: @children + @modal
└── (feed)/
    ├── layout.tsx          # Layout do Feed: @children + @sidebar
    ├── page.tsx            # Página Inicial do Feed
    └── photos/
        ├── [id]/
        │   └── page.tsx    # Página completa: /photos/123
        └── (.)[id]/
            └── page.tsx    # Interceptação: abre como modal no Feed

(3) Ganhos

Dimensão Antes (Roteamento Padrão) Depois (Paralelo + Interceptação)
Experiência de Navegação de Imagens Redirecionamento de página inteira, interrompe a navegação Pré-visualização em pop-up, permanece na página atual
Comportamento do Botão Voltar Sai do aplicativo Fecha o modal e retorna ao Feed
Página pode ser atualizada Não suportado Atualiza para exibir a página inteira; renderiza normalmente
Barra lateral é independente Não independente @sidebar Renderiza independentemente, sem afetar o conteúdo principal


3. Rotas Paralelas

(1) Conceitos e Uso

100%
graph TB
    subgraph "URL: /dashboard"
        A[layout.tsx] --> B[children<br/>Conteúdo Principal]
        A --> C[@modal<br/>Slot de Pop-up]
        A --> D[@sidebar<br/>Slot da Barra Lateral]
        A --> E[@team<br/>Slot da Equipe]
    end

    subgraph "Resultados da Renderização"
        F[Área de Conteúdo Principal] & G[Área de Pop-up] & H[Área da Barra Lateral] & I[Área da Equipe]
    end

    style A fill:#cce5ff
    style B fill:#d4edda
Nome do Slot Prefixo do Diretório Impacto na URL Caso de Uso
@children Nenhum (padrão) URL padrão Conteúdo principal da página
@modal @modal/ Nenhum Pop-ups, caixas de diálogo
@sidebar @sidebar/ Nenhum Painel da Barra Lateral
@team @team/ Nenhum Visão da Equipe

▶ Exemplo: Rotas Paralelas Básicas

Saída:

TEXT 📖 Somente leitura
Diagrama da estrutura de rotas: caminhos do sistema de arquivos mapeiam para caminhos de URL.
TSX
// ============================================
// Fundamentos de Rotas Paralelas: Layout Multi-slot do Dashboard
// ============================================

// src/app/(dashboard)/layout.tsx — Layout de Roteamento Paralelo
export default function DashboardLayout({
  children,
  modal,
  sidebar,
  team,
}: {
  children: React.ReactNode;
  modal: React.ReactNode;
  sidebar: React.ReactNode;
  team: React.ReactNode;
}) {
  return (
    <div className="flex h-screen">
      {/* Área de Conteúdo Principal */}
      <main className="flex-1 p-8 overflow-auto">
        {children}
      </main>

      {/* Slot da Barra Lateral (Renderização Paralela Independente) */}
      <aside className="w-72 bg-gray-50 p-4 border-l">
        {sidebar}
      </aside>

      {/* Slot da Equipe (Renderização Paralela Independente) */}
      <aside className="w-64 bg-gray-900 text-white p-4">
        {team}
      </aside>

      {/* Slot do Modal (Renderização Condicional) */}
      {modal}
    </div>
  );
}

Saída:

TEXT 📖 Somente leitura
Renderiza: Layout do Dashboard com barra lateral escura (links Dashboard/Projetos/Configurações) + área de conteúdo principal.
TSX
// src/app/(dashboard)/@sidebar/default.tsx — Estado Padrão da Barra Lateral
export default function SidebarDefault() {
  return (
    <div>
      <h3 className="font-bold text-lg mb-4">Barra Lateral</h3>
      <div className="space-y-2">
        <div className="p-3 bg-white rounded shadow-sm">
          <p className="font-medium">Atividade Recente</p>
          <p className="text-sm text-gray-500">Nenhuma atividade recente</p>
        </div>
        <div className="p-3 bg-white rounded shadow-sm">
          <p className="font-medium">Notificações</p>
          <p className="text-sm text-gray-500">3 não lidas</p>
        </div>
      </div>
    </div>
  );
}
TSX
// src/app/(dashboard)/@team/default.tsx — Estado Padrão do Slot da Equipe
export default function TeamDefault() {
  return (
    <div className="p-4">
      <h3 className="font-bold mb-4">Equipe</h3>
      <div className="space-y-3">
        {["Alice", "Bob", "Charlie", "Diana"].map(name => (
          <div key={name} className="flex items-center gap-2">
            <div className="w-8 h-8 bg-blue-500 rounded-full flex items-center justify-center text-white text-sm">
              {name[0]}
            </div>
            <span className="text-sm">{name}</span>
          </div>
        ))}
      </div>
    </div>
  );
}

Saída:

TEXT 📖 Somente leitura
Acesse /dashboard:
┌──────────────────────┬──────────┬──────────┐
│                      │          │          │
│   Área de Conteúdo Principal    │ Barra    │  Equipe  │
│   Dashboard           │ Recente  │  Alice   │
│   Bem-vindo de volta! │ Atividade│  Bob     │
│                      │ Notif(3) │  Charlie │
│                      │          │  Diana   │
└──────────────────────┴──────────┴──────────┘

Três regiões renderizadas independentemente, não se afetam mutuamente

(2) default.tsx — Estado padrão obrigatório

Cada diretório @slot deve conter default.tsx, que é exibido quando nenhuma rota correspondente é encontrada.

100%
graph TB
    A[Navegação do Usuário] --> B{A URL atual<br/>corresponde à rota do slot?}
    B -->|Corresponde| C[Mostrar page.tsx do slot]
    B -->|Não corresponde| D[Mostrar default.tsx do slot]

    style C fill:#d4edda
    style D fill:#f8d7da

▶ Exemplo: A Importância do default.tsx

Saída:

TEXT 📖 Somente leitura
Diagrama: Navegação do Usuário; Mostrar page.tsx do slot; Mostrar default.tsx do slot.
TSX
// ============================================
// Sem default.tsx ocorrerá 404
// Cada @slot deve ter default.tsx
// ============================================

// src/app/@modal/default.tsx — Modal oculto por padrão
export default function ModalDefault() {
  return null; // Sem Modal, não renderiza nada
}

// src/app/(dashboard)/@sidebar/default.tsx
export default function SidebarDefault() {
  return (
    <div className="p-4">
      <h3 className="font-bold text-sm text-gray-500 uppercase">
        Links Rápidos
      </h3>
      <nav className="mt-3 space-y-2">
        <a href="/dashboard" className="block text-blue-600">Dashboard</a>
        <a href="/dashboard/projects" className="block text-blue-600">Projetos</a>
        <a href="/dashboard/settings" className="block text-blue-600">Configurações</a>
      </nav>
    </div>
  );
}

Saída:

TEXT 📖 Somente leitura
Inclui uma barra lateral.

Saída:

TEXT 📖 Somente leitura
Acesse /dashboard:
- @modal Nenhuma rota correspondente encontrada → Mostra ModalDefault (= null, não renderiza)
- @sidebar Nenhuma rota correspondente encontrada → Mostra SidebarDefault (Navegação de Links Rápidos)
- @team Nenhuma rota correspondente encontrada → Mostra TeamDefault (Lista de Membros da Equipe)

Acesse /dashboard/photos/1 (Supondo que @modal encontrou correspondência):
- @modal corresponde → Exibe o conteúdo da janela pop-up
- Outros slots exibem seus respectivos default.tsx


4. Rotas de Interceptação

(1) Regras de Correspondência de Interceptação

100%
graph TB
    A[Atualmente no Feed] --> B{Clica no link da imagem}
    B --> C[Intercepta (.)photo]
    C --> D[Exibe um pop-up no Feed]
    D --> E[O usuário atualiza a página]
    E --> F[Ignora o Bloqueio<br/>Mostra a Página Inteira]

    style C fill:#cce5ff
    style D fill:#d4edda
    style F fill:#f8d7da
Sintaxe Nível de Correspondência Exemplo
(.) Mesmo Nível feed/photos/(.)[id] Intercepta feed/photos/[id]
(..) Um Nível Acima feed/(..)photos/[id] Intercepta photos/[id]
(..)(..) Dois Níveis Acima feed/(..)(..)photos/[id] Intercepta no nível raiz photos/[id]
(...) Nível Raiz feed/(...)photos/[id] Intercepta app/photos/[id]

▶ Exemplo: Bloqueando a Navegação de Imagens

Saída:

TEXT 📖 Somente leitura
Diagrama: Atualmente no Feed; Intercepta (.)photo; Exibe um pop-up no Feed; O usuário atualiza a página; Ignora o Bloqueio Mostra a Página Inteira.
TSX
// ============================================
// Rota de Interceptação + Rota Paralela
// Clique na imagem abaixo no Feed → Exibe uma janela pop-up na página atual
// ============================================

// Estrutura de Diretórios:
// app/
//   layout.tsx                    # Layout Raiz (com @modal)
//   (feed)/
//     page.tsx                    # Feed
//     photos/
//       [id]/page.tsx             # Página completa: /photos/1
//   @modal/
//     default.tsx                 # Sem modal
//     (.)photos/
//       [id]/page.tsx             # Intercepta: exibe um popup no Feed

// src/app/layout.tsx — Layout Raiz (com slot modal)
export default function RootLayout({
  children,
  modal,
}: {
  children: React.ReactNode;
  modal: React.ReactNode;
}) {
  return (
    <html lang="pt-BR">
      <body>
        {children}
        {modal}
      </body>
    </html>
  );
}

// src/app/(feed)/page.tsx — Página do Feed
import Link from "next/link";

export default function FeedPage() {
  const photos = Array.from({ length: 12 }, (_, i) => ({
    id: i + 1,
    url: `https://picsum.photos/seed/${i + 1}/300/300`,
    title: `Foto ${i + 1}`,
  }));

  return (
    <div className="max-w-4xl mx-auto p-8">
      <h1 className="text-3xl font-bold mb-6">Feed de Fotos</h1>
      <div className="grid grid-cols-3 gap-4">
        {photos.map(photo => (
          <Link
            key={photo.id}
            href={`/photos/${photo.id}`}
            className="block overflow-hidden rounded-lg hover:opacity-90 transition-opacity"
          >
            <img
              src={photo.url}
              alt={photo.title}
              className="w-full h-64 object-cover"
            />
            <p className="mt-2 text-sm font-medium text-center">{photo.title}</p>
          </Link>
        ))}
      </div>
    </div>
  );
}

Saída:

TEXT 📖 Somente leitura
Renderiza uma lista dinâmica de itens usando .map().
Conteúdo visível: Feed de Fotos
TSX
// ============================================
// Rotas de Interceptação: Abre um popup modal no Feed
// Arquivo: src/app/@modal/(.)photos/[id]/page.tsx
// ============================================

'use client';

import { useRouter } from "next/navigation";

export default function PhotoModal({
  params,
}: {
  params: Promise<{ id: string }>;
}) {
  const router = useRouter();
  const { id } = params;

  return (
    // Máscara de Fundo
    <div
      className="fixed inset-0 bg-black/70 flex items-center justify-center z-50"
      onClick={() => router.back()}
    >
      {/* Conteúdo do Modal — Clique no fundo para fechar */}
      <div
        className="bg-white rounded-2xl overflow-hidden max-w-2xl w-full mx-4"
        onClick={e => e.stopPropagation()}
      >
        <img
          src={`https://picsum.photos/seed/${id}/800/600`}
          alt={`Foto ${id}`}
          className="w-full h-auto"
        />
        <div className="p-6">
          <div className="flex items-center justify-between">
            <div>
              <h2 className="text-xl font-bold">Foto #{id}</h2>
              <p className="text-gray-500 text-sm mt-1">
                Capturada por fotógrafo
              </p>
            </div>
            <button
              onClick={() => router.back()}
              className="w-10 h-10 bg-gray-100 rounded-full flex items-center justify-center hover:bg-gray-200"
            >
              ✕
            </button>
          </div>
          <div className="flex gap-2 mt-4">
            <span className="px-3 py-1 bg-blue-100 text-blue-700 rounded-full text-sm">
              Natureza
            </span>
            <span className="px-3 py-1 bg-green-100 text-green-700 rounded-full text-sm">
              Paisagem
            </span>
            <span className="px-3 py-1 bg-purple-100 text-purple-700 rounded-full text-sm">
              HD
            </span>
          </div>
        </div>
      </div>
    </div>
  );
}
TSX
// ============================================
// Página completa: Exibe a página inteira quando acessada independentemente
// Arquivo: src/app/photos/[id]/page.tsx
// ============================================

import Link from "next/link";

export default function PhotoPage({ params }: { params: Promise<{ id: string }> }) {
  const { id } = params;

  return (
    <div className="max-w-4xl mx-auto p-8">
      <Link
        href="/"
        className="text-blue-600 hover:underline mb-4 inline-block"
      >
        &larr; Voltar ao Feed
      </Link>
      <img
        src={`https://picsum.photos/seed/${id}/1200/800`}
        alt={`Foto ${id}`}
        className="w-full rounded-lg"
      />
      <div className="mt-6">
        <h1 className="text-3xl font-bold">Foto #{id}</h1>
        <p className="text-gray-500 mt-2">
          Visualização em página inteira da foto {id}. Esta página é diretamente acessível
          e funciona mesmo sem JavaScript.
        </p>
        <div className="flex gap-4 mt-6">
          <Link
            href={`/photos/${Number(id) - 1}`}
            className="px-4 py-2 bg-gray-100 rounded-lg hover:bg-gray-200"
          >
            ← Anterior
          </Link>
          <Link
            href={`/photos/${Number(id) + 1}`}
            className="px-4 py-2 bg-gray-100 rounded-lg hover:bg-gray-200"
          >
            Próximo →
          </Link>
        </div>
      </div>
    </div>
  );
}

Saída:

TEXT 📖 Somente leitura
Cenário 1: Clique em uma imagem do Feed
- Atualmente em / Página (Grade do Feed de Fotos)
- Clique na 1ª foto
- Exibe uma imagem maior em uma janela pop-up (A barra de endereço muda para /photos/1)
- Clique na máscara de fundo → router.back() → Volta ao Feed
- Clique no botão voltar do navegador → Fecha o Pop-up → O Feed permanece inalterado

Cenário 2: Acesso Direto a /photos/1
- Ignora as Rotas de Interceptação
- Mostra a Página Completa (imagem grande em largura total + botões Anterior/Próximo)
- Pode ser atualizada diretamente, Link Compartilhável

Cenário 3: Atualize a página do pop-up
- Janela pop-up no Feed → Pressione F5 para atualizar
- As regras de interceptação não estão em efeito (Não veio da Navegação do Feed)
- Mostra o texto completo da página /photos/1

(2) Interceptação de Múltiplos Níveis

▶ Exemplo: Roteamento de Interceptação de Múltiplos Níveis

Saída:

TEXT 📖 Somente leitura
A página renderiza conforme descrito acima, com a UI atualizando com base no comportamento descrito.
TSX
// ============================================
// Exemplo de Roteamento de Interceptação de Múltiplos Níveis
// Estrutura de Diretórios:
// app/
//   photos/
//     [id]/page.tsx                    → /photos/1 (Página Completa)
//   (feed)/
//     page.tsx                         → / (Feed)
//     categories/
//       [cat]/page.tsx                 → /categories/nature (Página de Categoria)
//       (..)(..)photos/
//         [id]/page.tsx                → Intercepta /photos/1 → Pop-up na Página de Categorias
// ============================================

// Explicação das Regras de Correspondência:
// (feed)/categories/[cat] níveis = app/(feed)/categories/[cat]
// Objetivo: app/photos/[id] níveis = app/photos/[id]
// Você precisa subir dois níveis para corresponder → (..)(..)

// app/(feed)/categories/(..)(..)photos/[id]/page.tsx
'use client';
import { useRouter } from "next/navigation";

export default function CategoryPhotoModal({ params }) {
  const router = useRouter();

  return (
    <div
      className="fixed inset-0 bg-black/60 z-50 flex items-center justify-center"
      onClick={() => router.back()}
    >
      <div onClick={e => e.stopPropagation()} className="max-w-lg">
        <img
          src={`https://picsum.photos/seed/${params.id}/600/400`}
          alt=""
          className="rounded-lg"
        />
        <button
          onClick={() => router.back()}
          className="mt-2 px-4 py-2 bg-white rounded"
        >
          Fechar
        </button>
      </div>
    </div>
  );
}

Saída:

TEXT 📖 Somente leitura
CategoryPhotoModal renderiza sua UI.

Saída:

TEXT 📖 Somente leitura
De /categories/nature, clique na imagem:
1. Exibe uma imagem maior em uma janela pop-up (Barra de endereço /photos/1)
2. Clique em Fechar ou no fundo → router.back() → Volta para /categories/nature
3. Acesso Direto a /photos/1 → Mostra a Página Completa (Sem pop-ups)
4. A janela pop-up na página de categoria está funcionando corretamente


5. Combinação Modal + Rota Paralela

(1) Design da Arquitetura

100%
graph TB
    subgraph "Modal Controlado por URL"
        A[Ações do Usuário] --> B{Navega para /photos/1}
        B --> C[No Feed?]
        C -->|Sim| D[Slot @modal<br/>Corresponde e Intercepta Rotas]
        C -->|Não| E[Acesso Direto<br/>Mostra a Página Completa]
        D --> F[Exibição do Pop-up<br/>Barra de Endereço Atualizada]
        F --> G[Usuário Sai]
        G --> H[router.back()]
        H --> I[Volta ao Feed<br/>Modal Desaparece]
    end

    style D fill:#cce5ff
    style E fill:#d4edda
    style G fill:#f8d7da
Ação do Usuário Mudança de URL Estado do Modal Estado da Página
Clica na Imagem do Feed //photos/1 Mostra Pop-up Mantém o Feed em segundo plano
Fecha o Pop-up /photos/1/ Oculta Feed Inalterado
Atualiza /photos/1 /photos/1 Sem alterações Sem pop-ups Mostra a página completa
Compartilha /photos/1 Compartilhável Sem pop-ups Destinatários veem a página completa

▶ Exemplo: Modal Completo

Saída:

TEXT 📖 Somente leitura
Diagrama: Ações do Usuário; No Feed?; Slot @modal Corresponde e Intercepta Rotas; Acesso Direto Mostra a Página Completa; Exibição do Pop-up Barra de Endereço Atualizada; Usuário Sai.
TSX
// ============================================
// Modal Completo + Rota Paralela + Rota de Interceptação
// Implementa um sistema de pop-up "Atualizável, Compartilhável, Reversível"
// ============================================

// src/app/layout.tsx
export default function RootLayout({
  children,
  modal,
}: {
  children: React.ReactNode;
  modal: React.ReactNode;
}) {
  return (
    <html lang="pt-BR">
      <body>
        {children}
        {modal}
      </body>
    </html>
  );
}

// src/app/@modal/default.tsx
export default function Default() {
  return null;
}

// src/app/@modal/(.)photos/[id]/page.tsx — Bloqueia Pop-ups
'use client';
import { useRouter } from "next/navigation";

export default function PhotoModal({ params }) {
  const router = useRouter();

  return (
    <div
      className="fixed inset-0 bg-black/80 flex items-center justify-center z-50"
      onClick={() => router.back()}
    >
      <div
        className="bg-white rounded-xl max-w-3xl w-full mx-4 shadow-2xl"
        onClick={e => e.stopPropagation()}
      >
        <div className="flex justify-end p-2">
          <button
            onClick={() => router.back()}
            className="w-8 h-8 flex items-center justify-center hover:bg-gray-100 rounded-full"
          >
            ✕
          </button>
        </div>
        <img
          src={`https://picsum.photos/seed/${params.id}/800/600`}
          alt=""
          className="w-full"
        />
        <div className="p-6">
          <h2 className="text-2xl font-bold">Foto #{params.id}</h2>
          <div className="flex gap-4 mt-4">
            <a
              href={`/photos/${params.id}`}
              className="text-sm text-blue-600 hover:underline"
              onClick={() => router.push(`/photos/${params.id}`)}
            >
              Abrir em página completa →
            </a>
          </div>
        </div>
      </div>
    </div>
  );
}

// src/app/(feed)/page.tsx — Feed
import Link from "next/link";

export default function Feed() {
  return (
    <div className="max-w-6xl mx-auto p-8">
      <h1 className="text-3xl font-bold mb-6">Galeria de Fotos</h1>
      <div className="grid grid-cols-4 gap-4">
        {[1, 2, 3, 4, 5, 6, 7, 8].map(id => (
          <Link
            key={id}
            href={`/photos/${id}`}
            className="block group"
          >
            <div className="aspect-square bg-gray-100 rounded-lg overflow-hidden">
              <img
                src={`https://picsum.photos/seed/${id}/400/400`}
                alt=""
                className="w-full h-full object-cover group-hover:scale-105 transition-transform"
              />
            </div>
          </Link>
        ))}
      </div>
    </div>
  );
}

Saída:

TEXT 📖 Somente leitura
Renderiza: Componente de navegação usando useRouter para roteamento programático.

Saída:

TEXT 📖 Somente leitura
Fluxo do Usuário:

1. Acesse / → Veja a Grade de 8 Imagens
2. Clique na imagem #3 → Exibe uma imagem maior em uma janela pop-up (URL: /photos/3)
3. Clique no ✕ no canto superior direito → Fecha o Pop-up (URL: /)
4. Clique na imagem #5 novamente → Popup (URL: /photos/5)
5. Clique no botão Voltar do navegador → Fecha o Pop-up (URL: /)
6. Pressione o botão Voltar novamente → Sai do aplicativo (Comportamento Normal)

Cenário de Atualização:
1. Enquanto o pop-up está aberto, pressione F5 → Atualiza a página inteira
2. As regras de interceptação não estão em efeito → Mostra a Página Completa /photos/3
3. A página está funcionando corretamente, há um link de navegação para voltar

Cenários de Compartilhamento:
1. Copie a URL /photos/3 e envie para um amigo
2. O amigo abre → Vê a página completa (Não um pop-up)
3. SEO da Página Normal, todo o conteúdo é indexável


6. Renderização Condicional do Dashboard

(1) Renderizar visões diferentes com base no papel

100%
graph TB
    A[Layout do Dashboard] --> B{Papéis do Usuário}
    B -->|admin| C[Painel @admin]
    B -->|editor| D[Painel @editor]
    B -->|viewer| E[Painel @viewer]

    style A fill:#cce5ff
    style C fill:#d4edda
    style D fill:#d4edda
    style E fill:#d4edda

▶ Exemplo: Dashboard Orientado por Papel

Saída:

TEXT 📖 Somente leitura
Diagrama de acesso baseado em papel: admin (completo), editor (leitura/escrita), viewer (somente leitura).
TSX
// ============================================
// Renderização Condicional: Exibe Dashboard diferente conforme o papel
// ============================================

// src/app/(dashboard)/layout.tsx — Layout de Roteamento por Papel
export default function DashboardLayout({
  children,
  admin,
  editor,
  viewer,
}: {
  children: React.ReactNode;
  admin: React.ReactNode;
  editor: React.ReactNode;
  viewer: React.ReactNode;
}) {
  // Simula a obtenção do papel a partir de Cookie/Session
  const role = "admin";

  return (
    <div className="flex h-screen">
      {/* Conteúdo Principal */}
      <main className="flex-1 p-8">
        {children}
      </main>

      {/* Renderiza slots diferentes com base no papel */}
      <aside className="w-80 border-l p-4">
        {role === "admin" && admin}
        {role === "editor" && editor}
        {role === "viewer" && viewer}
      </aside>
    </div>
  );
}

Saída:

TEXT 📖 Somente leitura
Renderiza a UI do componente ▶ Exemplo: Dashboard Orientado por Papel conforme descrito na seção.
TSX
// src/app/(dashboard)/@admin/default.tsx — Painel do Administrador
export default function AdminPanel() {
  return (
    <div className="space-y-4">
      <h3 className="font-bold text-lg">Controles de Admin</h3>
      <div className="bg-red-50 border border-red-200 rounded-lg p-4">
        <p className="font-medium text-red-700">Saúde do Sistema</p>
        <div className="mt-2 space-y-2">
          <div className="flex justify-between text-sm">
            <span>Uso de CPU</span>
            <span className="text-green-600">45%</span>
          </div>
          <div className="flex justify-between text-sm">
            <span>Memória</span>
            <span className="text-yellow-600">72%</span>
          </div>
          <div className="flex justify-between text-sm">
            <span>Usuários Ativos</span>
            <span className="text-blue-600">1.234</span>
          </div>
        </div>
      </div>
      <div className="bg-white rounded-lg border p-4">
        <p className="font-medium">Aprovações Pendentes</p>
        <p className="text-2xl font-bold text-orange-600 mt-2">12</p>
      </div>
      <button className="w-full p-2 bg-blue-600 text-white rounded-lg">
        Ver Todas as Configurações
      </button>
    </div>
  );
}

// src/app/(dashboard)/@editor/default.tsx — Painel do Editor
export default function EditorPanel() {
  return (
    <div className="space-y-4">
      <h3 className="font-bold text-lg">Ferramentas do Editor</h3>
      <div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
        <p className="font-medium text-blue-700">Contagem de Rascunhos</p>
        <p className="text-3xl font-bold mt-2">8</p>
      </div>
      <div className="bg-white rounded-lg border p-4">
        <p className="font-medium">Edições Recentes</p>
        <div className="mt-2 space-y-2 text-sm">
          <p>• Atualizou o hero da página inicial</p>
          <p>• Corrigiu erro de digitação na página sobre</p>
          <p>• Adicionou nova postagem no blog</p>
        </div>
      </div>
      <button className="w-full p-2 bg-green-600 text-white rounded-lg">
        Criar Nova Postagem
      </button>
    </div>
  );
}

// src/app/(dashboard)/@viewer/default.tsx — Painel do Visualizador
export default function ViewerPanel() {
  return (
    <div className="space-y-4">
      <h3 className="font-bold text-lg">Visão Geral</h3>
      <div className="bg-gray-50 border rounded-lg p-4">
        <p className="font-medium">Seu Dashboard</p>
        <p className="text-sm text-gray-500 mt-2">
          Você tem acesso somente leitura. Entre em contato com o admin para permissões de edição.
        </p>
      </div>
      <div className="bg-white border rounded-lg p-4">
        <p className="font-medium">Links Rápidos</p>
        <div className="mt-2 space-y-2 text-sm">
          <a href="/docs" className="block text-blue-600">Documentação</a>
          <a href="/reports" className="block text-blue-600">Relatórios</a>
          <a href="/help" className="block text-blue-600">Central de Ajuda</a>
        </div>
      </div>
    </div>
  );
}

Saída:

TEXT 📖 Somente leitura
Administrador (role = "admin"):
┌────────────────────────────┬──────────────────────┐
│                            │  Controles de Admin   │
│    Dashboard               │  ┌────────────────┐  │
│    Bem-vinda, Alice!       │  │ CPU: 45% ✅     │  │
│                            │  │ Memória: 72% ⚠️ │  │
│    Resumo do Projeto       │  │ Usuários: 1.234 │  │
│    12 Projetos Ativos      │  └────────────────┘  │
│    48 Tarefas Pendentes    │  Aprovações Pendentes │
│    8 Membros da Equipe     │  12                   │
│                            │  [Ver Configurações]  │
└────────────────────────────┴──────────────────────┘

Editor (role = "editor"):
┌────────────────────────────┬──────────────────────┐
│                            │  Ferramentas Editor   │
│    Dashboard               │  Rascunhos: 8         │
│    Bem-vindo, Bob!         │  Edições Recentes:    │
│                            │  • Atualizou homepage │
│    Meus Projetos           │  • Corrigiu erro      │
│    5 Projetos Ativos       │  • Adicionou post     │
│                            │  [Criar Nova Postagem]│
└────────────────────────────┴──────────────────────┘

Visualizador (role = "viewer"):
┌────────────────────────────┬──────────────────────┐
│                            │  Visão Geral          │
│    Dashboard               │  Acesso somente       │
│    Bem-vindo, Charlie!     │  leitura              │
│                            │  Links Rápidos:       │
│    Atividade da Equipe     │  • Documentação       │
│    10 membros online       │  • Relatórios         │
│                            │  • Central de Ajuda   │
└────────────────────────────┴──────────────────────┘


7. Exemplo Completo: Feed + Modal + Dashboard Condicional

TSX
// ============================================
// Exemplo Abrangente: Sistema de Roteamento Completo para Plataformas de Conteúdo
// Cobrindo Rotas Paralelas + Rotas de Interceptação + Dashboard Condicional
// ============================================

// src/app/layout.tsx — Layout Raiz (com slot modal)
export default function RootLayout({
  children,
  modal,
}: {
  children: React.ReactNode;
  modal: React.ReactNode;
}) {
  return (
    <html lang="pt-BR">
      <body className="bg-gray-50">
        <header className="bg-white shadow-sm sticky top-0 z-40">
          <div className="max-w-6xl mx-auto px-4 py-3 flex justify-between">
            <a href="/" className="text-xl font-bold text-blue-600">PhotoVault</a>
            <nav className="flex gap-4">
              <a href="/" className="hover:text-blue-600">Feed</a>
              <a href="/dashboard" className="hover:text-blue-600">Dashboard</a>
            </nav>
          </div>
        </header>
        {children}
        {modal}
      </body>
    </html>
  );
}

// src/app/@modal/default.tsx
export default function Default() {
  return null;
}

// src/app/@modal/(.)photos/[id]/page.tsx — Bloqueia Pop-ups
'use client';
import { useRouter } from "next/navigation";

export default function PhotoModal({ params }) {
  const router = useRouter();

  return (
    <div
      className="fixed inset-0 bg-black/70 flex items-center justify-center z-50"
      onClick={() => router.back()}
    >
      <div
        className="bg-white rounded-xl max-w-2xl w-full mx-4 overflow-hidden shadow-2xl"
        onClick={e => e.stopPropagation()}
      >
        <img
          src={`https://picsum.photos/seed/${params.id}/800/600`}
          alt=""
          className="w-full"
        />
        <div className="p-4 flex justify-between items-center">
          <div>
            <h2 className="font-bold">Foto #{params.id}</h2>
            <p className="text-sm text-gray-500">Clique no fundo para fechar</p>
          </div>
          <button
            onClick={() => router.push(`/photos/${params.id}`)}
            className="text-sm text-blue-600 hover:underline"
          >
            Abrir Página Completa
          </button>
        </div>
      </div>
    </div>
  );
}

// src/app/(feed)/page.tsx — Feed
import Link from "next/link";

export default function FeedPage() {
  const photos = Array.from({ length: 12 }, (_, i) => ({
    id: i + 1,
    url: `https://picsum.photos/seed/${i + 1}/400/400`,
    title: `Foto ${i + 1}`,
  }));

  return (
    <div className="max-w-6xl mx-auto p-8">
      <h1 className="text-3xl font-bold mb-6">Feed de Fotos</h1>
      <div className="grid grid-cols-4 gap-4">
        {photos.map(photo => (
          <Link
            key={photo.id}
            href={`/photos/${photo.id}`}
            className="block aspect-square rounded-lg overflow-hidden bg-gray-100"
          >
            <img
              src={photo.url}
              alt={photo.title}
              className="w-full h-full object-cover hover:scale-105 transition-transform"
            />
          </Link>
        ))}
      </div>
    </div>
  );
}

// src/app/photos/[id]/page.tsx — Página Completa da Imagem
export default async function PhotoPage({ params }) {
  const { id } = params;

  return (
    <div className="max-w-4xl mx-auto p-8">
      <a href="/" className="text-blue-600 hover:underline mb-4 inline-block">
        &larr; Voltar ao Feed
      </a>
      <img
        src={`https://picsum.photos/seed/${id}/1200/800`}
        alt=""
        className="w-full rounded-lg shadow-lg"
      />
      <div className="mt-6">
        <h1 className="text-3xl font-bold">Foto #{id}</h1>
        <p className="text-gray-500 mt-2">
          Esta é a visualização em página inteira. Compartilhe este link diretamente com outros.
        </p>
      </div>
    </div>
  );
}

// src/app/(dashboard)/layout.tsx — Layout de Roteamento Paralelo do Dashboard
export default function DashboardLayout({
  children,
  admin,
}: {
  children: React.ReactNode;
  admin: React.ReactNode;
}) {
  const role = "admin";

  return (
    <div className="max-w-6xl mx-auto p-8 flex gap-8">
      <div className="flex-1">{children}</div>
      <aside className="w-80">
        {role === "admin" && admin}
      </aside>
    </div>
  );
}

// src/app/(dashboard)/@admin/default.tsx
export default function AdminPanel() {
  return (
    <div className="bg-white rounded-xl shadow-sm border p-6 space-y-4">
      <h3 className="font-bold text-lg">Painel Admin</h3>
      <div className="space-y-2">
        <div className="flex justify-between">
          <span>Total de Fotos</span>
          <span className="font-bold">1.234</span>
        </div>
        <div className="flex justify-between">
          <span>Uploads Diários</span>
          <span className="font-bold text-green-600">+48</span>
        </div>
        <div className="flex justify-between">
          <span>Armazenamento Usado</span>
          <span className="font-bold">237 GB</span>
        </div>
      </div>
      <button className="w-full p-2 bg-blue-600 text-white rounded-lg">
        Gerenciar Galeria
      </button>
    </div>
  );
}

// src/app/(dashboard)/page.tsx — Página Inicial do Dashboard
export default function DashboardPage() {
  return (
    <div>
      <h1 className="text-2xl font-bold">Dashboard</h1>
      <p className="text-gray-500 mt-2">Visão geral da sua galeria de fotos</p>
      <div className="grid grid-cols-2 gap-4 mt-6">
        <div className="bg-white p-6 rounded-xl shadow-sm border">
          <p className="text-sm text-gray-500">Esta Semana</p>
          <p className="text-3xl font-bold mt-1">342</p>
          <p className="text-sm text-green-600 mt-1">↑ 12% em relação à semana passada</p>
        </div>
        <div className="bg-white p-6 rounded-xl shadow-sm border">
          <p className="text-sm text-gray-500">Total de Visualizações</p>
          <p className="text-3xl font-bold mt-1">89,4K</p>
          <p className="text-sm text-green-600 mt-1">↑ 8% em relação ao mês passado</p>
        </div>
      </div>
    </div>
  );
}

Saída Esperada:

TEXT 📖 Somente leitura
1. Acesse / → Grade de 12 Imagens
2. Clique na imagem → Exibe uma imagem maior em uma janela pop-up (URL: /photos/5)
3. Clique no fundo → Fecha o Pop-up, Volta ao Feed
4. Atualize → Mostra a Página Completa /photos/5
5. Compartilhe /photos/5 → Amigos veem a página inteira
6. Acesse /dashboard → Mostra Dashboard + Painel Admin
7. Do Dashboard, clique na imagem → Mesma experiência de popup (Rotas de Interceptação)

❓ Perguntas Frequentes

P: Todo @slot precisa de um arquivo default.tsx? R: Sim. Se não houver um arquivo default.tsx, o Next.js retornará um erro 404 quando a URL atual não corresponder a uma rota para aquele slot. Mesmo que seu slot modal não seja exibido na maioria das vezes, você ainda precisa de export default function Default() { return null; }.

P: Os slots nas Rotas Paralelas podem se comunicar entre si? R: Eles não podem se comunicar diretamente. Cada slot é um Server Component independente e não pode compartilhar estado. Se for necessária comunicação, você pode passar valores via parâmetros de URL (searchParams) ou usar um Context Provider no layout raiz.

P: Como o nível de (..) para Rotas de Interceptação é calculado? R: (..) é calculado com base na hierarquia real do sistema de arquivos, mas Grupos de Rotas (group) não consomem níveis. Por exemplo, app/(feed)/photos/(.)[id]/page.tsx intercepta app/(feed)/photos/[id]/page.tsx porque (feed) não é contado.

P: O que acontece quando você acessa diretamente uma URL no modo Modal + Rota de Interceptação? R: Quando você acessa diretamente (ou atualiza) a página, a interceptação de rota não entra em efeito, e o usuário vê a página completa e independente. Essa é a beleza desta abordagem — ela alcança "degradação graciosa" através da interceptação de rota, permitindo que a URL seja acessada e compartilhada diretamente.

P: As Rotas Paralelas afetam a performance da página? R: Há um leve impacto porque cada slot é um componente servidor independente que precisa ser renderizado separadamente. No entanto, o Next.js paraleliza automaticamente essas requisições, tornando-o mais rápido que a renderização serial. Recomendação: Não use mais de 3–4 slots e evite slots desnecessários.

P: A renderização condicional deve usar Rotas Paralelas ou ser tratada no lado do cliente? R: Recomendamos Rotas Paralelas. Embora a renderização condicional no lado do cliente (if (role === 'admin')) também seja possível, as Rotas Paralelas mantêm o arquivo de visão de cada papel independente, type-safe e mais fácil de testar e separar.


📖 Resumo


📝 Exercícios

  1. Exercício Básico (⭐): Crie um slot @modal no seu projeto e implemente a funcionalidade básica de "clicar no botão → exibir um trecho de texto em um popup" (use default.tsx para retornar null).

  2. Exercício Avançado (⭐⭐): Crie um sistema de navegação de imagens: A página Feed (/) exibe uma grade de imagens; clicar em uma imagem abre uma janela pop-up ((.)photos/[id]); e acessar diretamente /photos/1 exibe a página completa. Verifique o comportamento do botão voltar e da função de atualização.

  3. Desafio (⭐⭐⭐): Implemente um sistema de dashboard orientado por papel: Crie três slots — @admin, @editor e @viewer — para que cada papel exiba conteúdo diferente do dashboard (administradores veem monitoramento do sistema, editores veem estatísticas de rascunhos e visualizadores veem avisos de somente leitura). Verifique a renderização condicional alternando a variável role.

Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%