Rotas Paralelas e Interceptadas no Next.js: Modais, Slots e Padrões Avançados
Rotas paralelas e interceptadas no Next.js App Router permitem dashboards com painéis independentes e modais com URL própria. Guia completo com código.
Rotas paralelas e interceptadas no Next.js App Router são duas convenções de pasta que permitem renderizar múltiplas páginas no mesmo layout (rotas paralelas, prefixadas com @) e capturar uma navegação para exibi-la em um contexto diferente, como um modal sobre a página atual (rotas interceptadas, prefixadas com (.), (..) ou (...)). Juntas, elas resolvem dois problemas que no Pages Router exigiam estado global, portais e bibliotecas de terceiros: dashboards com várias seções independentes e modais com URL própria que sobrevivem ao refresh.
Rotas paralelas usam pastas prefixadas com @ (slots) que aparecem como props no layout.tsx pai e renderizam em paralelo na mesma URL.
Cada slot precisa de um default.tsx para o que renderizar quando a rota atual não corresponde; esquecer disso é a causa #1 de telas em branco em produção.
Rotas interceptadas ((.), (..), (...)) capturam a navegação soft e renderizam outra rota no contexto atual, mantendo a URL deep-linkable.
O padrão clássico é combinar os dois: um slot @modal com rota interceptada (..)photo/[id] implementa modais que sobrevivem a refresh sem estado global.
No Next.js 16, esses padrões funcionam com Cache Components e Partial Prerendering, mas exigem cuidado com "use cache" em slots dinâmicos.
Os hooks useSelectedLayoutSegment(s) permitem ler qual rota está ativa em cada slot, útil para abas e estados condicionais no layout.
O que são rotas paralelas no Next.js?
Rotas paralelas são pastas dentro do App Router cujos nomes começam com @, chamadas de slots. Cada slot vira uma prop do layout pai e renderiza em paralelo aos demais, sem alterar a URL. Em vez de uma única árvore de rotas mapeando para um único children, você tem várias árvores simultâneas, útil para dashboards com painéis independentes, dual-panes de leitura/edição ou layouts onde sidebar e conteúdo principal navegam em ritmos diferentes.
Na prática, isso era impossível no Pages Router sem hacks: cada página era uma única árvore. No App Router, declarar um slot é uma operação de filesystem. Eu venho do Pages Router e demorei para internalizar isso. Slots não são uma feature de runtime, é a estrutura de pastas que cria o paralelismo. O React renderiza tudo, o Next.js decide o que streamar primeiro com base em quem termina antes (RSC streaming).
Um exemplo mínimo: imagine uma página /dashboard com um painel de métricas e um feed de atividade. Cada um vem de um serviço diferente, com latências diferentes. Com slots, você os declara como rotas separadas:
Cada slot tem seu próprio loading.tsx e error.tsx, o que significa que uma falha no feed de atividade não derruba o painel de métricas. Em produção, esse isolamento é o que mais me convenceu a migrar dashboards inteiros do Pages Router.
Estrutura de pastas, slots e o papel do default.tsx
Aqui mora a primeira armadilha. Quando o usuário navega para uma sub-rota que existe em children mas não tem correspondência em um slot, o Next.js precisa saber o que renderizar nesse slot. A resposta é default.tsx: um componente obrigatório em cada slot que serve de fallback quando a URL atual não casa com nenhuma rota dele. Sem ele, em hard navigation (refresh, deep link) o slot fica como null e o React lança um erro de renderização, e em produção isso aparece como tela branca, não como um erro óbvio.
O ponto sutil: em soft navigation (clique em <Link>), o Next.js preserva o estado anterior do slot. Já em hard navigation, ele precisa do default.tsx. Por isso bugs com slots quase sempre aparecem só depois de F5. Eu tomei esse F5 na cara umas três vezes antes da lição grudar.
// app/dashboard/@metrics/default.tsx
export default function MetricsDefault() {
// Pode retornar null se faz sentido visualmente,
// mas é melhor mostrar um placeholder neutro.
return <div className="text-sm text-muted">Sem métricas disponíveis.</div>
}
Boa prática: trate default.tsx como parte do design, não como obrigação burocrática. Em dashboards, o estado "sem rota correspondente" é uma oportunidade de mostrar dados padrão (últimos 7 dias, métrica mais recente) em vez de um vazio. Para um aprofundamento em como os slots interagem com pré-renderização parcial, vale a leitura do nosso guia de Cache Components e Partial Prerendering no Next.js 16.
Como funcionam as rotas interceptadas?
Rotas interceptadas permitem capturar uma navegação para uma rota A e renderizá-la no contexto de uma rota B. O caso de uso canônico: o usuário está em /feed, clica em uma foto que tem URL própria /photo/123, e em vez de sair do feed você abre essa foto como modal sobreposto. Mas se ele compartilhar a URL /photo/123 ou der refresh, a página completa de foto é renderizada, sem hack, sem estado global, deep-linkable de graça.
A convenção é uma pasta com prefixo numérico de pontos entre parênteses, indicando quantos níveis subir antes de fazer a interceptação:
(.)pasta: intercepta no mesmo nível
(..)pasta: intercepta um nível acima
(..)(..)pasta: intercepta dois níveis acima
(...)pasta: intercepta a partir da raiz do app
O nome confunde porque parece com ../ de filesystem, mas a contagem é em níveis de segmento de rota, não em pastas físicas. Eu pessoalmente errei isso umas seis vezes. Group routes (grupo) não contam como segmentos, então (..) não "sobe" através delas.
O @modal/default.tsx é crucial. Em qualquer navegação que não bata com a rota interceptada, ele renderiza nada:
// app/@modal/default.tsx
export default function ModalDefault() {
return null
}
A rota interceptada app/@modal/(..)photo/[id]/page.tsx renderiza o modal de fato. Note como ela usa um Server Component para buscar os dados, exatamente o mesmo padrão da página completa, sem duplicação de fetching:
O ModalShell é um Client Component que cuida do overlay, foco e fechamento via router.back(). Como o modal está no slot @modal, fechá-lo é apenas voltar uma entrada do histórico: a URL /photo/123 sai e o feed volta intacto, sem perder posição de scroll. Esse padrão se beneficia de organização sólida de dados, e fica ainda melhor quando combinado com Server Actions para mutações no formulário do modal.
Qual a diferença entre (.), (..) e (...)?
A confusão entre os marcadores vem de tratar a contagem como filesystem em vez de segmentos de URL. Aqui está uma tabela de referência rápida que eu mantenho colada no meu monitor:
Marcador
Significado
Exemplo de uso
Intercepta
(.)
Mesmo nível de segmento
feed/(.)photo
/feed/photo dentro de /feed
(..)
Um segmento acima
@modal/(..)photo
/photo/* a partir da raiz
(..)(..)
Dois segmentos acima
@modal/(..)(..)settings
Dois níveis acima
(...)
A partir da raiz do app/
(...)photo
Qualquer profundidade desde raiz
Regra que me poupa tempo: group routes não contam. Se você tem app/(marketing)/feed/page.tsx e quer interceptar app/photo/[id] a partir do feed, (..) resolve, porque (marketing) não é um segmento real de URL. A interceptação opera sobre o path final, não sobre as pastas no disco.
Outro detalhe que pega muita gente: interceptação só funciona em soft navigation. Se o usuário cola a URL na barra de endereço ou abre em nova aba, a rota interceptada nunca é acionada, e isso é o comportamento desejado. É por isso que você precisa ter a versão "página inteira" da rota também, pronta para deep-link.
Loading e error states por slot
Cada slot pode (e deveria) ter loading.tsx e error.tsx próprios. Isso é o que torna dashboards realmente robustos: você pode mostrar skeletons independentes enquanto cada painel carrega no seu próprio ritmo. Combinado com Suspense streaming, isso gera uma experiência de carregamento muito superior ao Pages Router, onde tudo bloqueava no getServerSideProps.
Para abas que mudam qual sub-rota está ativa em um slot, use useSelectedLayoutSegment (singular) ou useSelectedLayoutSegments (plural) dentro de um Client Component no layout. Você lê qual segmento está montado em qual slot e ajusta o destaque visual da nav:
// components/dashboard-tabs.tsx
'use client'
import { useSelectedLayoutSegment } from 'next/navigation'
import Link from 'next/link'
export function DashboardTabs() {
const active = useSelectedLayoutSegment('metrics') ?? 'overview'
return (
<nav>
<Link href="/dashboard/metrics/overview" aria-current={active === 'overview'}>Visão geral</Link>
<Link href="/dashboard/metrics/revenue" aria-current={active === 'revenue'}>Receita</Link>
</nav>
)
}
O 'metrics' passado para o hook é o nome do slot (sem o @). Isso libera UIs de abas onde cada aba é uma sub-rota completa, com fetching próprio e cache próprio, sem precisar de useState.
Rotas paralelas com Cache Components no Next.js 16
No Next.js 16, Cache Components e Partial Prerendering convivem bem com slots, mas há uma sutileza: cada slot tem seu próprio limite de Suspense implícito, e o "use cache" aplicado em uma sub-página de um slot é avaliado independentemente. Você pode ter um slot @metrics totalmente estático (com "use cache") ao lado de um slot @activity totalmente dinâmico (com cookies()), na mesma URL.
// app/dashboard/@metrics/page.tsx (estático, cacheável)
'use cache'
import { getMetrics } from '@/lib/metrics'
export default async function Metrics() {
const data = await getMetrics()
return <MetricsCards data={data} />
}
// app/dashboard/@activity/page.tsx (dinâmico)
import { cookies } from 'next/headers'
import { getActivityFor } from '@/lib/activity'
export default async function Activity() {
const userId = (await cookies()).get('uid')?.value
const items = await getActivityFor(userId)
return <ActivityList items={items} />
}
O Partial Prerendering vai pré-renderizar a shell + slot @metrics e streamar o @activity ao chegar a request. Isso é praticamente impossível de orquestrar manualmente. Slots fazem por estrutura. Documentação oficial do Partial Prerendering tem o blueprint completo.
Armadilhas comuns e como depurar
Em ordem de frequência com que essas me mordem em produção:
Faltou default.tsx: tela branca após F5. Verifique cada slot, não só o que você acabou de editar. Eu mantenho um teste de smoke que dá refresh em cada rota crítica.
Interceptação não dispara: o usuário deve chegar via <Link> do Next.js, não <a> nem digitação na barra. Verifique o nível de (..) contra segmentos reais de URL, ignorando group routes.
Modal duplica conteúdo: você definiu @modal/(..)photo/[id]/page.tsx mas esqueceu de filtrar a rota normal, ou colocou o slot dentro de outro layout que também renderiza children. Cheque que o slot está no nível certo da árvore.
Refresh dentro do modal: sem app/photo/[id]/page.tsx como página inteira, F5 dá 404. A interceptação não substitui a rota base, ela coexiste.
Hydration mismatch em slots: se useSelectedLayoutSegment é chamado em layout dinâmico, o segmento muda entre server e cliente. Mova para um Client Component ou marque o layout como dinâmico explicitamente.
Group routes confundem a contagem: repetindo porque é a #1 fonte de bugs sutis. Use (...) com slug a partir da raiz se a contagem ficar ambígua.
Para depurar, ative NEXT_DEBUG=true e olhe o overlay do dev: ele mostra qual slot está renderizando o quê. Quando algo não bate, geralmente é um arquivo default.tsx faltando ou um (..) mal contado. Em mais de 90% dos casos que eu vi, o bug é um desses dois.
Perguntas frequentes
Posso usar rotas paralelas sem rotas interceptadas?
Sim. Slots paralelos (@nome) são úteis sozinhos para dashboards com painéis independentes, sidebars com navegação própria, ou qualquer UI onde duas regiões da tela precisam carregar e atualizar em paralelo. A interceptação é uma camada opcional.
Rotas interceptadas funcionam com refresh ou deep link?
Não. A interceptação só dispara em soft navigation via componente <Link>. Em refresh, abertura em nova aba, ou cole de URL, o Next.js renderiza a rota original (app/photo/[id]/page.tsx). É por isso que você precisa manter as duas versões: a página inteira e a versão modal.
Como faço para fechar um modal de rota interceptada?
Use router.back() dentro de um Client Component. Como o modal foi aberto por <Link>, voltar uma entrada do histórico remove a URL /photo/123 e o slot @modal cai no default.tsx (que retorna null). Bind também a tecla Escape e clique no overlay para a mesma ação.
Posso ter mais de um slot interceptado simultaneamente?
Sim. Você pode ter @modal, @drawer, @sidebar, cada um interceptando rotas diferentes. Cuide para que default.tsx de cada um lide com os casos em que outro slot está ativo. Em testes, navegue por todas as combinações que sua UI permite (modal aberto, drawer aberto, ambos ao mesmo tempo se o design permitir).
Rotas paralelas afetam SEO?
O slot é renderizado no HTML final na URL principal, então o Googlebot enxerga o conteúdo normalmente. A interceptação não cria URLs separadas, então não há páginas duplicadas. Para SEO, garanta que a rota completa (/photo/[id]) tenha metadata via generateMetadata e seja indexável. O modal é só uma experiência de navegação secundária.
Guia prático para integrar Drizzle ORM no Next.js 16 com Server Components, Server Actions, Edge runtime, connection pooling e o novo cache explícito. Padrões de produção testados em projetos reais.
O Next.js 16 trocou middleware.ts por proxy.ts, migrando do Edge Runtime para Node.js. Aprenda a migrar com codemod, integrar Auth.js e next-intl, e evitar os erros mais comuns.
Domine Server Actions no Next.js: desde formulários com progressive enhancement até padrões avançados como useActionState, useOptimistic, validação com Zod e segurança de endpoints públicos.