Rotas Paralelas
Rotas paralelas usam pastas @slot para renderizar múltiplas páginas simultaneamente dentro de um único layout. Cada slot é um segmento de rota independente com seu próprio estado de carregamento, erro e navegação.
Busque em todas as páginas da documentação
Rotas paralelas usam pastas @slot para renderizar múltiplas páginas simultaneamente dentro de um único layout. Cada slot é um segmento de rota independente com seu próprio estado de carregamento, erro e navegação.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Cartão de receita de referência rápida - pronto para copiar e colar.
app/
├── layout.tsx # Recebe @analytics e @team como props
├── page.tsx # Conteúdo principal para /
├── @analytics/
│ ├── page.tsx # Painel de analytics para /
│ ├── default.tsx # Fallback para sub-rotas não correspondentes
│ └── loading.tsx # Estado de carregamento independente
├── @team/
│ ├── page.tsx # Painel de equipe para /
│ └── default.tsx # Fallback para sub-rotas não correspondentes
// app/layout.tsx
export default function Layout({
children,
analytics,
team,
}: {
children: React.ReactNode;
analytics: React.ReactNode;
team: React.ReactNode;
}) {
return (
<div>
{children}
<div className="grid grid-cols-2 gap-4">
{analytics}
{team}
</div>
</div>
);
}Quando usar isso: Dashboards com painéis independentes, conteúdo condicional baseado em autenticação, ou qualquer layout que componha múltiplas visualizações independentes.
// app/dashboard/layout.tsx - Dashboard com três slots paralelos
export default function DashboardLayout({
children,
metrics,
activity,
}: {
children: React.ReactNode;
metrics: React.ReactNode;
activity: React.ReactNode;
}) {
return (
<div className="p-6">
<h1 className="mb-6 text-2xl font-bold">Dashboard</h1>
<div className="mb-6">{children}</div>
<div className="grid grid-cols-2 gap-6">
<section>
<h2 className="mb-2 text-lg font-semibold">Métricas</h2>
{metrics}
</section>
<section>
<h2 className="mb-2 text-lg font-semibold">Atividade</h2>
{activity}
</section>
</div>
</div>
);
}// app/dashboard/@metrics/page.tsx - Slot de Métricas
async function getMetrics() {
const res = await fetch("https://api.example.com/metrics", {
next: { revalidate: 60 },
});
return res.json();
}
export default async function MetricsPanel() {
const metrics = await getMetrics();
return (
<div className="space-y-3 rounded-lg border p-4">
<div className="flex justify-between">
<span>Receita</span>
<span className="font-bold">${metrics.revenue.toLocaleString()}</span>
</div>
<div className="flex justify-between">
<span>Usuários</span>
<span className="font-bold">{metrics.users.toLocaleString()}</span>
</div>
<div className="flex justify-between">
<span>Conversão</span>
<span className="font-bold">{metrics.conversion}%</span>
</div>
</div>
);
}// app/dashboard/@metrics/loading.tsx - Estado de carregamento independente
export default function MetricsLoading() {
return (
<div className="space-y-3 rounded-lg border p-4">
{Array.from({ length: 3 }).map((_, i) => (
<div key={i} className="flex justify-between">
<div className="h-4 w-20 animate-pulse rounded bg-gray-200" />
<div className="h-4 w-16 animate-pulse rounded bg-gray-200" />
</div>
))}
</div>
);
}// app/dashboard/@activity/page.tsx - Slot de Atividade
async function getActivity() {
const res = await fetch("https://api.example.com/activity", {
next: { revalidate: 30 },
});
return res.json();
}
export default async function ActivityPanel() {
const items = await getActivity();
return (
<ul className="space-y-2 rounded-lg border p-4">
{items.map((item: { id: string; message: string; time: string }) => (
<li key={item.id} className="flex justify-between text-sm">
<span>{item.message}</span>
<span className="text-gray-500">{item.time}</span>
</li>
))}
</ul>
);
}// app/dashboard/@metrics/default.tsx - Fallback obrigatório
export default function MetricsDefault() {
return null;
}
// app/dashboard/@activity/default.tsx - Fallback obrigatório
export default function ActivityDefault() {
return null;
}// app/dashboard/page.tsx - Conteúdo principal (renderizado em {children})
export default function DashboardPage() {
return (
<p className="text-gray-600">
Bem-vindo ao seu dashboard. Métricas e atividade carregam independentemente abaixo.
</p>
);
}@folder cria um slot nomeado. O prefixo @ informa ao Next.js para passar o slot como uma prop para o layout pai. @metrics se torna a prop metrics.children é um slot implícito. O page.tsx no mesmo diretório do layout é automaticamente o slot children. Você não cria @children.loading.tsx e error.tsx. Cada slot transmite e captura erros independentemente, então uma API lenta não bloqueia o dashboard inteiro.default.tsx fornece um fallback. Quando um slot não tem uma sub-rota correspondente, default.tsx é renderizado em vez disso. Isso previne erros 404 durante a navegação.@metrics/page.tsx não adiciona /metrics à URL. A URL é determinada pelos segmentos que não são slots.// Renderização condicional baseada em autenticação
// app/dashboard/layout.tsx
import { auth } from "@/lib/auth";
export default async function DashboardLayout({
children,
admin,
user,
}: {
children: React.ReactNode;
admin: React.ReactNode;
user: React.ReactNode;
}) {
const session = await auth();
const isAdmin = session?.user?.role === "admin";
return (
<div>
{children}
{isAdmin ? admin : user}
</div>
);
}
// app/dashboard/@admin/page.tsx - Mostrado para administradores
// app/dashboard/@user/page.tsx - Mostrado para usuários regulares// Slots com sub-rotas
// app/dashboard/@metrics/detailed/page.tsx
// Navegar para /dashboard/detailed renderiza isso no slot @metrics
// enquanto @activity mostra seu default.tsx (ou sua própria página /detailed)
export default function DetailedMetrics() {
return <div>Visualização de métricas detalhadas</div>;
}# Rotas paralelas com navegação independente
app/dashboard/
├── layout.tsx
├── page.tsx
├── @left/
│ ├── page.tsx # Painel esquerdo padrão
│ ├── default.tsx
│ └── inbox/page.tsx # /dashboard/inbox → atualiza o painel esquerdo
├── @right/
│ ├── page.tsx # Painel direito padrão
│ ├── default.tsx
│ └── inbox/page.tsx # /dashboard/inbox → atualiza o painel direito também
// Layout com slots de rota paralela
interface DashboardLayoutProps {
children: React.ReactNode; // Slot implícito (page.tsx)
metrics: React.ReactNode; // Slot @metrics
activity: React.ReactNode; // Slot @activity
}
// Páginas de slot têm as mesmas props que páginas regulares
interface SlotPageProps {
params: Promise<Record<string, string>>;
searchParams: Promise<Record<string, string | string[] | undefined>>;
}
// default.tsx não recebe props
// loading.tsx não recebe props
// error.tsx recebe { error, reset } (deve ser "use client")default.tsx é crítico. Sem ele, navegar para uma sub-rota que um slot tem, mas outro não, causa um 404. Sempre crie default.tsx em cada slot.default.tsx. Se não houver default.tsx, o Next.js renderiza um 404.@metrics nunca aparece na URL. Não link para /@metrics/something.@slot dentro de outro @slot.@metrics no mesmo nível causarão conflito.children e slots nomeados compartilham a mesma URL. Quando a URL é /dashboard/settings, o Next.js procura por /dashboard/settings/page.tsx (children), /dashboard/@metrics/settings/page.tsx, e /dashboard/@activity/settings/page.tsx. Páginas ausentes recorrem a default.tsx.page.tsx e default.tsx. Para três slots, isso são seis arquivos antes de adicionar tratamento de loading e erro.| Abordagem | Quando Usar |
|---|---|
Página única com limites <Suspense> | Código mais simples quando navegação independente não é necessária |
| Abas ou painéis no lado do cliente | Quando a troca de painéis é puramente visual, não baseada em URL |
| Rotas de Interceptação | Quando você quer uma sobreposição modal com uma URL compartilhável |
| Server Components com streaming | Quando você quer busca de dados paralela sem roteamento paralelo |
| Grupos de Rotas | Organizando rotas sem visualizações simultâneas múltiplas |
O prefixo @ cria um slot nomeado. O Next.js passa o slot como uma prop para o layout pai. Por exemplo, @metrics se torna a prop metrics no componente layout.
O page.tsx no mesmo diretório do layout serve automaticamente como o slot children. Você não precisa criar uma pasta @children.
Navegar para uma sub-rota que um slot tem, mas outro não, causa um 404. Em navegação rígida (refresh), slots não correspondentes sem default.tsx também renderizam um 404. Sempre crie default.tsx em cada slot.
Não. Slots são invisíveis na URL. @metrics/page.tsx não adiciona /metrics ao caminho. Não link para /@metrics/something.
Sim. Cada slot transmite e captura erros independentemente. Uma API lenta em um slot não bloqueia o renderizado do dashboard inteiro.
// app/dashboard/layout.tsx
import { auth } from "@/lib/auth";
export default async function Layout({
children,
admin,
user,
}: {
children: React.ReactNode;
admin: React.ReactNode;
user: React.ReactNode;
}) {
const session = await auth();
const isAdmin = session?.user?.role === "admin";
return (
<div>
{children}
{isAdmin ? admin : user}
</div>
);
}Não. Todos os slots devem ser filhos diretos do diretório do layout. Slots aninhados não são suportados.
interface DashboardLayoutProps {
children: React.ReactNode;
metrics: React.ReactNode;
activity: React.ReactNode;
}Cada slot é tipado como React.ReactNode.
Quando a URL é /dashboard/settings, o Next.js procura por:
/dashboard/settings/page.tsx (children)/dashboard/@metrics/settings/page.tsx/dashboard/@activity/settings/page.tsxPáginas ausentes em um slot recorrem ao default.tsx desse slot.
Sim. Quando slots resolvem em momentos diferentes, o layout pode pular. Use dimensões fixas ou placeholders de esqueleto com alturas consistentes para prevenir layout shift.
Cada slot precisa no mínimo de page.tsx e default.tsx. Para três slots, isso são seis arquivos antes de adicionar loading.tsx e error.tsx.
Ambos não recebem props. São componentes simples:
// default.tsx
export default function Default() {
return null;
}
// loading.tsx
export default function Loading() {
return <div>Carregando...</div>;
}Revisado por Chris St. John·Última atualização: 19 de jul. de 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥