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
- Rotas Paralelas: Renderizando múltiplas visões independentes a partir de uma única URL
- Como Usar os Slots Nomeados
@modal,@sidebare@team - Rotas de Interceptação: Regras de correspondência para
(.),(..)e(...) - Combinação Modal + Rota Paralela: Modais Controlados por URL
- Renderização condicional: Renderizar diferentes visões do dashboard com base no papel do usuário
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
@modalpara exibir uma janela pop-up com roteamento paralelo +(.)photopara interceptar a navegação de imagens no feed.
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
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:
Diagrama da estrutura de rotas: caminhos do sistema de arquivos mapeiam para caminhos de URL.
// ============================================
// 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:
Renderiza: Layout do Dashboard com barra lateral escura (links Dashboard/Projetos/Configurações) + área de conteúdo principal.
// 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>
);
}
// 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:
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.
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:
Diagrama: Navegação do Usuário; Mostrar page.tsx do slot; Mostrar default.tsx do slot.
// ============================================
// 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:
Inclui uma barra lateral.
Saída:
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
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:
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.
// ============================================
// 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:
Renderiza uma lista dinâmica de itens usando .map().
Conteúdo visível: Feed de Fotos
// ============================================
// 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>
);
}
// ============================================
// 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"
>
← 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:
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:
A página renderiza conforme descrito acima, com a UI atualizando com base no comportamento descrito.
// ============================================
// 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:
CategoryPhotoModal renderiza sua UI.
Saída:
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
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:
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.
// ============================================
// 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:
Renderiza: Componente de navegação usando useRouter para roteamento programático.
Saída:
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
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:
Diagrama de acesso baseado em papel: admin (completo), editor (leitura/escrita), viewer (somente leitura).
// ============================================
// 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:
Renderiza a UI do componente ▶ Exemplo: Dashboard Orientado por Papel conforme descrito na seção.
// 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:
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
// ============================================
// 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">
← 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:
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.tsxinterceptaapp/(feed)/photos/[id]/page.tsxporque(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
- Rotas Paralelas: Implementando Múltiplas Áreas de Renderização Independentes para uma Única URL Usando a Convenção de Nomenclatura
@slot - Cada
@slotdeve conterdefault.tsx; exibido quando nenhuma rota correspondente é encontrada - Rotas de Interceptação: Use
(.),(..),(..)(..)e(...)para corresponder rotas em diferentes níveis - A combinação "Modal + Rota de Interceptação" implementa "pop-ups controlados por URL", equilibrando experiência do usuário e SEO
- A interceptação de rota só entra em efeito quando o cliente navega; o acesso direto ou a atualização da página exibe a página inteira
(...)corresponde a rotas no nível raiz,(.)corresponde a rotas no mesmo nível,(..)corresponde a rotas no nível pai- A renderização condicional é implementada usando slots de Rotas Paralelas e verificações baseadas em papel, com a visão de cada papel em um arquivo separado
- Grupos de Rotas
(group)Não consomem níveis no cálculo de interceptação
📝 Exercícios
-
Exercício Básico (⭐): Crie um slot
@modalno seu projeto e implemente a funcionalidade básica de "clicar no botão → exibir um trecho de texto em um popup" (usedefault.tsxpara retornarnull). -
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/1exibe a página completa. Verifique o comportamento do botão voltar e da função de atualização. -
Desafio (⭐⭐⭐): Implemente um sistema de dashboard orientado por papel: Crie três slots —
@admin,@editore@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ávelrole.