App Router do Next.js 15: Layouts, Loading States e Error Boundaries na Prática
O problema que o App Router resolve (e o que ele cria)
O Pages Router do Next.js tratava cada rota como uma ilha. Compartilhar estado de layout entre páginas exigia _app.tsx, _document.tsx e uma coreografia frágil de getLayout patterns. O App Router inverte essa lógica: a árvore de arquivos define a árvore de componentes. Layouts persistem entre navegações, loading states operam por segmento, e error boundaries capturam falhas sem derrubar a aplicação inteira.
Mas essa inversão traz armadilhas. Layouts que re-renderizam quando não deveriam. Loading states que cobrem a tela inteira em vez de um trecho. Error boundaries que engolem erros silenciosamente. Este post mostra como montar cada peça com precisão e onde a maioria dos projetos erra.
Layouts: persistência que funciona a seu favor
Um layout.tsx no App Router é um React Server Component por padrão. Ele recebe children e não re-renderiza quando o usuário navega entre rotas filhas. Isso significa que um sidebar, um header ou um provider de tema sobrevive à navegação sem perder estado.
// app/dashboard/layout.tsx
import { ReactNode } from "react";
import { Sidebar } from "@/components/sidebar";
import { getSession } from "@/lib/auth";
import { redirect } from "next/navigation";
// Server Component: roda apenas no servidor, sem bundle pro client
export default async function DashboardLayout({
children,
}: {
children: ReactNode;
}) {
const session = await getSession();
// Redireciona antes de renderizar qualquer coisa,
// evitando flash de conteúdo protegido
if (!session) {
redirect("/login");
}
return (
<div className="flex min-h-screen">
<Sidebar user={session.user} />
<main className="flex-1 p-6">{children}</main>
</div>
);
}
O getSession() roda no servidor a cada request, mas o layout em si não re-monta no client durante navegação client-side. O React preserva a árvore do layout e troca apenas o children. Isso é diferente de um _app.tsx no Pages Router, que re-executava a cada transição.
Layouts aninhados com escopo preciso
A composição real aparece quando você aninha layouts. Cada segmento de rota pode ter seu próprio layout, e eles se empilham:
// app/dashboard/projects/layout.tsx
import { ReactNode } from "react";
import { ProjectNav } from "@/components/project-nav";
export default function ProjectsLayout({
children,
}: {
children: ReactNode;
}) {
return (
<div>
<ProjectNav />
{/* Apenas essa região troca quando o usuário navega
entre /dashboard/projects/[id] */}
<section className="mt-4">{children}</section>
</div>
);
}
A estrutura de pastas app/dashboard/layout.tsx + app/dashboard/projects/layout.tsx produz dois layouts encaixados. Quando o usuário navega de /dashboard/projects/1 para /dashboard/projects/2, o DashboardLayout e o ProjectsLayout persistem. Apenas o page.tsx dentro de projects/[id] re-renderiza.
Se você precisa de proteção de rotas nesse fluxo, o layout do dashboard já cuida disso. O layout de projects não precisa repetir a verificação. Para estratégias mais avançadas de autenticação com middleware, o post sobre autenticação com NextAuth.js e Middleware cobre o fluxo completo.
Loading states: granularidade por segmento
O arquivo loading.tsx cria um Suspense boundary automático ao redor do page.tsx do mesmo segmento. Quando o componente da página é um Server Component assíncrono, o Next.js mostra o loading.tsx enquanto aguarda a resolução.
// app/dashboard/projects/loading.tsx
export default function ProjectsLoading() {
return (
<div className="grid grid-cols-3 gap-4">
{Array.from({ length: 6 }).map((_, i) => (
<div
key={i}
className="h-32 animate-pulse rounded-lg bg-zinc-800"
/>
))}
</div>
);
}
// app/dashboard/projects/page.tsx
import { Proje
---
Leia o artigo completo em [https://www.vivodecodigo.com.br/nextjs/nextjs-app-router-layouts-loading-error-boundaries-pratica](https://www.vivodecodigo.com.br/nextjs/nextjs-app-router-layouts-loading-error-boundaries-pratica)