Next.js: Layouts e Modelos (Templates)
Última atualização: 2026-08-26
Um sistema de layout é como os andares de um prédio — cada andar (layout) possui corredores e instalações compartilhadas, enquanto cada sala (página) pode ter uma decoração diferente, e o estado dos usuários do espaço é preservado conforme eles se movem entre os andares.
1. O Que Você Vai Aprender
- Comportamento de Persistência e Métodos de Configuração para Layouts Aninhados
- O Impacto do Agrupamento Lógico e URLs nos Grupos de Rotas
(group) - Principais Diferenças Entre Layouts e Modelos (Templates) e Quando Usar Cada Um
- Configurações essenciais para o layout raiz:
<html>,<body>, fontes, metadados - Três Modelos para Compartilhamento de Dados Entre Layouts
2. Uma História Real de uma Desenvolvedora Full-Stack
(1) Ponto de Dor: Ter que escrever repetidamente a barra de navegação e a barra lateral para cada página
Ao desenvolver o painel administrativo do TaskFlow, Alice encontrou um problema de duplicação de código de layout:
"Nossa equipe tem cinco desenvolvedores, cada um trabalhando em páginas diferentes. Todos precisam importar manualmente a barra de navegação e a barra lateral em seus próprios arquivos page.tsx. Na semana passada, Charlie esqueceu de adicionar a barra lateral na página recém-criada
settings/page.tsx, então quando um usuário clicou na página de configurações, o menu desapareceu de repente, e eles pensaram que a navegação estava quebrada."
Duplicação de Código:
| Problema | Impacto | Páginas Afetadas |
|---|---|---|
| Importações duplicadas da barra de navegação | Deve ser incluída manualmente em cada página | 15 páginas |
| Estado da Barra Lateral Não é Persistente | O estado selecionado na barra lateral é perdido após navegar | Todas as Páginas |
| Navegação exibida na página de login/cadastro | Não deveria ser exibida; requer verificações condicionais adicionais | 3 páginas |
| Transmissão complexa de dados do usuário | Os dados do usuário precisam ser buscados em cada página | 12 páginas |
(2) Soluções com o Sistema de Layout do Next.js
Use layouts aninhados e Grupos de Rotas para isolar layouts; defina uma vez para aplicação global.
src/app/
├── layout.tsx # Layout Raiz (html/body/Fonte)
├── page.tsx # Página Inicial
├── (auth)/
│ ├── layout.tsx # Layout de Login/Cadastro (Sem barra lateral)
│ ├── login/page.tsx
│ └── register/page.tsx
└── (dashboard)/
├── layout.tsx # Layout do Painel Admin (Navegação+Barra Lateral)
├── page.tsx # Dashboard
├── projects/page.tsx # Lista de Projetos
└── settings/page.tsx # Página de Configurações
(3) Ganhos
| Dimensão | Antes (importação manual) | Depois (sistema de layout) |
|---|---|---|
| Referências da Barra de Navegação | 15 linhas de import em 15 páginas |
1 arquivo layout.tsx |
| Estado de Seleção da Barra Lateral | Perdido | Persistente |
| Layout de Login/Cadastro | Verificações Condicionais | Isolamento Natural via Grupos de Rotas |
| Busca de Dados do Usuário | 12 buscas | 1 layout compartilhado |
3. Princípios dos Layouts Aninhados
(1) Comportamento de Persistência do Layout
graph TB
subgraph "Navegação de Página"
A[layout raiz] --> B[layout dashboard]
B --> C[página Dashboard]
B --> D[página Lista de Projetos]
B --> E[página Configurações]
end
subgraph "Comportamento Durante a Navegação"
F[layout Mantém Montado<br/>Sem perda de estado]
G[página Desmonta/Remonta<br/>Substituição de Conteúdo]
end
style F fill:#d4edda
style G fill:#f8d7da
| Comportamento | layout | page |
|---|---|---|
| Remonta durante a navegação | ❌ Não remonta | ✅ Remonta |
| Preservação do Estado React | ✅ Preservado | ❌ Reiniciado |
| useEffect reexecuta | ❌ Não executa | ✅ Executa |
| Busca Dados | ❌ Não busca | ✅ Busca |
▶ Exemplo: Demonstração de Persistência do Layout
Saída:
Diagrama: o layout persiste durante a navegação (mantém o estado), o template remonta (nova instância).
// ============================================
// Demonstração de Persistência: Layout vs Página Remontada
// ============================================
// src/app/(dashboard)/layout.tsx — Layout com Barra Lateral
'use client';
import { useState } from "react";
import Link from "next/link";
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
const [sidebarState, setSidebarState] = useState("collapsed");
return (
<div className="flex h-screen">
{/* Barra Lateral — Mantém expandida/recolhida durante a navegação */}
<aside className={`bg-gray-800 text-white ${sidebarState === "collapsed" ? "w-16" : "w-64"} transition-all`}>
<button
onClick={() => setSidebarState(s =>
s === "collapsed" ? "expanded" : "collapsed"
)}
className="p-4 hover:bg-gray-700 w-full text-left"
>
{sidebarState === "collapsed" ? "→" : "← Recolher"}
</button>
<nav className="mt-4">
<Link href="/dashboard" className="block p-3 hover:bg-gray-700">Dashboard</Link>
<Link href="/dashboard/projects" className="block p-3 hover:bg-gray-700">Projetos</Link>
<Link href="/dashboard/settings" className="block p-3 hover:bg-gray-700">Configurações</Link>
</nav>
<div className="mt-4 p-3 text-sm text-gray-400">
O estado da barra lateral persiste durante a navegação
</div>
</aside>
<main className="flex-1 p-8 overflow-auto">
{children}
</main>
</div>
);
}
Saída:
Componente interativo com estado: sidebarState.
// src/app/(dashboard)/dashboard/page.tsx
export default function DashboardPage() {
return (
<div>
<h1 className="text-2xl font-bold">Dashboard</h1>
<p className="text-gray-600">Bem-vindo ao seu painel de controle.</p>
</div>
);
}
// src/app/(dashboard)/dashboard/projects/page.tsx
export default function ProjectsPage() {
return (
<div>
<h1 className="text-2xl font-bold">Projetos</h1>
<p className="text-gray-600">Sua lista de projetos aparece aqui.</p>
</div>
);
}
Saída:
1. Acesse /dashboard, Expanda a barra lateral, Veja o Conteúdo do Dashboard
2. Clique em "← Recolher", A barra lateral recolhe para 64px
3. Clique no link "Projetos", Navegue para /dashboard/projects
4. A barra lateral permanece recolhida (Não foi redefinida para expandida) ✅
5. O conteúdo da página muda de Dashboard para Projetos ✅
(2) Hierarquia de Layouts Aninhados
graph TB
A[layout raiz] --> B[app/layout.tsx]
B --> C[layout (dashboard)]
C --> D[app/(dashboard)/layout.tsx]
D --> E[layout products]
E --> F[app/(dashboard)/products/layout.tsx]
F --> G[página]
G --> H[app/(dashboard)/products/page.tsx]
style B fill:#cce5ff
style D fill:#d4edda
style F fill:#f8d7da
| Nível de Layout | Escopo | Conteúdo Compartilhado |
|---|---|---|
| Layout Raiz | Todas as Páginas | <html>, <body>, Fontes Globais, Estilos Globais |
| Layout de Grupo | Páginas dentro de um grupo | Barra lateral, barra de navegação, informações do usuário |
| Layout Aninhado | Páginas de Subdiretório | Subnavegação, Breadcrumbs, Barra de Filtro Local |
▶ Exemplo: Layout aninhado de três níveis
Saída:
Diagrama da hierarquia de layout aninhado: raiz → pai → layouts filhos.
// ============================================
// Layout aninhado de três níveis: Global → Backend → Gerenciamento de Produtos
// ============================================
// Nível 1: src/app/layout.tsx — Layout Raiz
export default function RootLayout({ children }) {
return (
<html lang="pt-BR">
<body className="bg-gray-50">
{children}
</body>
</html>
);
}
// Nível 2: src/app/(dashboard)/layout.tsx — Layout do Backend
export default function DashboardLayout({ children }) {
return (
<div className="flex">
<Sidebar />
<main className="flex-1">{children}</main>
</div>
);
}
// Nível 3: src/app/(dashboard)/products/layout.tsx — Portfólio de Produtos
export default function ProductsLayout({ children }) {
return (
<div>
<nav className="flex gap-4 border-b pb-2 mb-4">
<a href="/products" className="text-blue-600">Todos os Produtos</a>
<a href="/products/add" className="text-blue-600">Adicionar Produto</a>
<a href="/products/categories" className="text-blue-600">Categorias</a>
</nav>
{children}
</div>
);
}
Saída:
RootLayout renderiza sua UI.
Saída:
Acesse /dashboard/products:
→ Layout Raiz Renderizando <html><body>
→ Layout do Backend Renderizando <Sidebar> + <main>
→ Layout de Produtos Renderizando Sub-Navegação de Produtos + Conteúdo da Página
Todos os três níveis de aninhamento estão em efeito
4. Grupos de Rotas (group)
(1) O Que São Grupos de Rotas?
Diretórios (group) não geram segmentos de caminho nas URLs; são usados exclusivamente para agrupamento lógico.
graph LR
subgraph "Estrutura de Arquivos"
A[app] --> B[(auth)]
A --> C[(dashboard)]
B --> D[login/page.tsx]
B --> E[register/page.tsx]
C --> F[page.tsx]
C --> G[settings/page.tsx]
end
subgraph "URL Correspondente"
H[/login]
I[/register]
J[/dashboard]
K[/dashboard/settings]
end
style B fill:#f8d7da
style C fill:#d4edda
| Diretório | Caminho da URL | Descrição |
|---|---|---|
app/(auth)/login/page.tsx |
/login |
(auth) Não gera segmentos de caminho |
app/(auth)/register/page.tsx |
/register |
(auth) Não gera segmentos de caminho |
app/(dashboard)/page.tsx |
/dashboard |
(dashboard) Não gera segmentos de caminho |
app/(dashboard)/settings/page.tsx |
/dashboard/settings |
Subcaminho é normal |
▶ Exemplo: Usando Grupos de Rotas para Implementar Layouts Diferentes
Saída:
Diagrama da estrutura de rotas: caminhos do sistema de arquivos mapeiam para caminhos de URL.
// ============================================
// Usando Grupos de Rotas para Separar os layouts da página de login e do painel admin
// ============================================
// src/app/(auth)/layout.tsx — Layout de Login/Cadastro (Sem Navegação, Sem barra lateral)
export default function AuthLayout({ children }) {
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-blue-50 to-indigo-100">
<div className="w-full max-w-md">
<div className="text-center mb-8">
<h1 className="text-3xl font-bold text-gray-900">TaskFlow</h1>
<p className="text-gray-500">Colabore e entregue</p>
</div>
<div className="bg-white p-8 rounded-xl shadow-sm">
{children}
</div>
</div>
</div>
);
}
// src/app/(auth)/login/page.tsx
export default function LoginPage() {
return (
<form className="space-y-4">
<h2 className="text-xl font-bold text-center">Entrar</h2>
<input
type="email"
placeholder="Email"
className="w-full p-3 border rounded-lg"
/>
<input
type="password"
placeholder="Senha"
className="w-full p-3 border rounded-lg"
/>
<button
type="submit"
className="w-full p-3 bg-blue-600 text-white rounded-lg"
>
Entrar
</button>
</form>
);
}
Saída:
Renderiza: TaskFlow | Colabore e entregue
// src/app/(dashboard)/layout.tsx — Layout do Painel Admin (Barra Lateral + Navegação Superior)
export default function DashboardLayout({ children }) {
return (
<div className="flex h-screen">
<aside className="w-64 bg-gray-900 text-white">
<div className="p-4 text-xl font-bold">TaskFlow</div>
<nav className="mt-4">
<a href="/dashboard" className="block p-3 hover:bg-gray-800">
Dashboard
</a>
<a href="/dashboard/projects" className="block p-3 hover:bg-gray-800">
Projetos
</a>
<a href="/dashboard/settings" className="block p-3 hover:bg-gray-800">
Configurações
</a>
</nav>
</aside>
<div className="flex-1 flex flex-col">
<header className="bg-white shadow-sm p-4">
<input
type="search"
placeholder="Pesquisar..."
className="w-64 p-2 border rounded"
/>
</header>
<main className="flex-1 p-8 overflow-auto">{children}</main>
</div>
</div>
);
}
Saída:
Acesse /login e veja:
- Design de Formulário Centralizado (Sem barra de navegação, Sem barra lateral)
- Fundo com gradiente azul
- Logotipo da Marca TaskFlow + Formulário de Entrar
Acesse /dashboard e veja:
- Barra lateral escura à esquerda (Dashboard / Projetos / Configurações)
- Barra de pesquisa branca no topo
- Área de conteúdo à direita
5. Layout vs Modelo (Template)
(1) Principais Diferenças
graph LR
A[Navegar para uma nova página] --> B{layout ou template?}
B -->|layout| C[Mantém Montado<br/>Estado Persistente]
B -->|template| D[Desinstala e Reinstala<br/>Remonta]
C --> E[Componente Filho Atualiza]
D --> F[Componente filho + Todos os contêineres wrapper foram reconstruídos]
style C fill:#d4edda
style D fill:#f8d7da
| Características | layout.tsx |
template.tsx |
|---|---|---|
| Remonta durante a navegação | ❌ Mantém montado | ✅ Desmonta + Remonta |
| Preservação do Estado React | ✅ Preservado | ❌ Reiniciado |
| useEffect reexecuta | ❌ Não executa | ✅ Executa |
| Animações de Transição de Página | Não adequado | ✅ Adequado |
| Atualizar Dados | ❌ Não atualiza | ✅ Atualiza a cada navegação |
| Performance | Melhor | Ligeiramente pior (sobrecarga de reconstrução) |
▶ Exemplo: Comparação de Comportamento entre Layout e Template
Saída:
Diagrama: o layout persiste durante a navegação (mantém o estado), o template remonta (nova instância).
// ============================================
// Demonstração de Comparação de Comportamento: layout vs template
// ============================================
// src/app/(dashboard)/layout.tsx — Usando layout (Mantém o estado durante a navegação)
'use client';
import { useEffect, useState } from "react";
import Link from "next/link";
export default function DashboardLayout({ children }) {
const [count, setCount] = useState(0);
const [mountTime] = useState(new Date().toLocaleTimeString());
useEffect(() => {
console.log("Layout montado em:", new Date().toLocaleTimeString());
}, []);
return (
<div className="border-2 border-blue-500 p-4 rounded m-4">
<div className="text-sm text-blue-600 mb-2">
[LAYOUT] Montado em: {mountTime} | Contador: {count}
<button onClick={() => setCount(c => c + 1)} className="ml-2 px-2 bg-blue-100 rounded">
+1
</button>
</div>
<nav className="flex gap-4 mb-4">
<Link href="/dashboard/page-a" className="text-blue-600">Página A</Link>
<Link href="/dashboard/page-b" className="text-blue-600">Página B</Link>
</nav>
{children}
</div>
);
}
Saída:
Componente interativo com estado: count.
// ============================================
// template.tsx — Reconstruído Durante a Navegação
// Arquivo: src/app/(dashboard)/template.tsx
// ============================================
'use client';
import { useEffect, useState } from "react";
import Link from "next/link";
export default function DashboardTemplate({ children }) {
const [count, setCount] = useState(0);
const [mountTime] = useState(new Date().toLocaleTimeString());
useEffect(() => {
console.log("Template montado em:", new Date().toLocaleTimeString());
}, []);
return (
<div className="border-2 border-red-500 p-4 rounded m-4">
<div className="text-sm text-red-600 mb-2">
[TEMPLATE] Montado em: {mountTime} | Contador: {count}
<button onClick={() => setCount(c => c + 1)} className="ml-2 px-2 bg-red-100 rounded">
+1
</button>
</div>
<nav className="flex gap-4 mb-4">
<Link href="/dashboard/page-a" className="text-red-600">Página A</Link>
<Link href="/dashboard/page-b" className="text-red-600">Página B</Link>
</nav>
{children}
</div>
);
}
// src/app/(dashboard)/page-a.tsx
export default function PageA() {
return <div className="text-lg">Conteúdo da Página A</div>;
}
// src/app/(dashboard)/page-b.tsx
export default function PageB() {
return <div className="text-lg">Conteúdo da Página B</div>;
}
Saída:
1. Acesse /dashboard/page-a:
[LAYOUT] Montado em: 10:30:00 | Contador: 0
[TEMPLATE] Montado em: 10:30:00 | Contador: 0
Conteúdo da Página A
2. Clique no botão Contador +1 (os contadores dos dois contêineres incrementam independentemente)
3. Clique no link "Página B":
[LAYOUT] Montado em: 10:30:00 | Contador: 1 ← layout Mantido, Estado Preservado
[TEMPLATE] Montado em: 10:30:05 | Contador: 0 ← template Reconstruído, Estado Reiniciado
Conteúdo da Página B
Conclusão: layout Mantém Montagem e Estado, template é reconstruído a cada navegação
(2) Quando Usar um Template
| Cenário | Recomendação | Motivo |
|---|---|---|
| Animações de Transição de Página | template | Animações de Entrada/Saída do framer-motion |
| Dados devem ser atualizados a cada navegação | template | useEffect reexecuta |
| Rastreamento de Análise de Navegação | template | Aciona o código de rastreamento a cada navegação |
| Barra de Navegação/Barra Lateral | layout | Mantém selecionado e expandido/recolhido |
| Carrinho de Compras/Player | layout | Estado de UI Persistente |
▶ Exemplo: Implementando animações de transição de página usando templates
Saída:
O componente renderiza a UI descrita no navegador.
// ============================================
// template + framer-motion Implementando Animações de Transição de Página
// ============================================
'use client';
import { motion } from "framer-motion";
export default function Template({ children }: { children: React.ReactNode }) {
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.3 }}
>
{children}
</motion.div>
);
}
Saída:
Renderiza a UI do componente Template.
Saída:
Toda vez que você navega para uma página:
1. A página antiga desaparece para cima (opacity: 1→0, y: 0→-20)
2. A nova página surge de baixo (opacity: 0→1, y: 20→0)
3. Duração da Animação: 300ms
4. layout Mantido, Não Reconstruído, Animações se aplicam apenas à área de conteúdo
6. Configuração Essencial para Layouts Raiz
(1) Responsabilidades do Layout Raiz
O layout raiz é o único arquivo de layout obrigatório; ele define a estrutura para todas as páginas:
| Opção de Configuração | Código | Descrição |
|---|---|---|
Tag <html> |
<html lang="pt-BR"> |
Atributos de idioma que afetam o SEO |
Tag <body> |
<body className="..."> |
Classe CSS Global |
| Carregamento de Fontes | next/font/google |
Fontes Otimizadas para Performance |
| Metadados | export const metadata |
Metadados Globais de SEO |
| Estilos Globais | import './globals.css' |
Diretivas do Tailwind |
▶ Exemplo: Layout Raiz Completo
Saída:
A página renderiza conforme descrito acima, com a UI atualizando com base no comportamento descrito.
// ============================================
// Layout Raiz — Configuração Completa
// Arquivo: src/app/layout.tsx
// ============================================
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
// Otimização de Fonte Google (Pré-carregamento automático + CSS size-adjust)
const inter = Inter({
subsets: ["latin"],
display: "swap",
variable: "--font-inter",
});
// Metadados globais de SEO
export const metadata: Metadata = {
title: {
template: "%s | TaskFlow",
default: "TaskFlow - Gerenciamento de Projetos",
},
description: "Uma plataforma colaborativa de gerenciamento de projetos",
openGraph: {
title: "TaskFlow",
description: "Colabore e entregue projetos mais rapidamente",
},
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="pt-BR" className={inter.variable}>
<body className="antialiased bg-gray-50 text-gray-900 min-h-screen">
{children}
</body>
</html>
);
}
Saída:
Renderiza: Layout raiz com fonte Inter, classes CSS globais e template de metadados SEO ("%s | TaskFlow").
Saída:
Todas as páginas são geradas automaticamente com:
- Fonte Inter (Otimização de Performance, zero CLS)
- Classes CSS globais (antialiased, bg-gray-50, text-gray-900)
- Metadados SEO (template de título "Página | TaskFlow")
- Tags Open Graph (Cartões de Compartilhamento em Redes Sociais)
7. Modelos de Compartilhamento de Dados
(1) Três Modelos de Compartilhamento
graph TB
subgraph "Modelos de Compartilhamento de Dados"
A[1. Passagem de Props<br/>layout → página]
B[2. Context Provider<br/>Estado Global]
C[3. Busca de Dados Paralela<br/>layout + página Obtêm individualmente]
end
style A fill:#d4edda
style B fill:#cce5ff
style C fill:#f8d7da
| Modo | Cenários Aplicáveis | Vantagens | Desvantagens |
|---|---|---|---|
| Passagem de Props | layout busca dados e passa para a página | type-safe | passa apenas um nível abaixo |
| Context Provider | Informações do usuário, temas | Disponível globalmente | Necessário para Client Component |
| Busca de Dados Paralela | Dados independentes para layout e página | Desacoplamento, concorrência | Não pode ser compartilhado diretamente |
8. Exemplo Completo: Sistema de Layout Completo do TaskFlow
// ============================================
// Exemplo Abrangente: Sistema de Layout Completo do TaskFlow
// Layout Raiz + Layout de Autenticação + Layout do Dashboard + Portfólio de Produtos
// ============================================
// src/app/layout.tsx — Layout Raiz
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
const inter = Inter({ subsets: ["latin"], display: "swap" });
export const metadata: Metadata = {
title: { template: "%s | TaskFlow", default: "TaskFlow" },
description: "Plataforma de gerenciamento de projetos",
};
export default function RootLayout({ children }) {
return (
<html lang="pt-BR" className={inter.className}>
<body className="bg-gray-50 antialiased">{children}</body>
</html>
);
}
// src/app/(auth)/layout.tsx — Layout de Autenticação
export default function AuthLayout({ children }) {
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-blue-600 to-indigo-700">
<div className="w-full max-w-md">
<div className="text-center text-white mb-8">
<h1 className="text-4xl font-bold">TaskFlow</h1>
<p className="text-blue-200 mt-2">Colabore e entregue</p>
</div>
<div className="bg-white rounded-xl shadow-2xl p-8">
{children}
</div>
</div>
</div>
);
}
// src/app/(dashboard)/layout.tsx — Layout do Dashboard
'use client';
import { createContext, useContext, useState } from "react";
import Link from "next/link";
const DashboardContext = createContext(null);
export function useDashboard() { return useContext(DashboardContext); }
export default function DashboardLayout({ children }) {
const [sidebarOpen, setSidebarOpen] = useState(true);
const user = { name: "Alice", role: "Admin", avatar: "/avatar.png" };
return (
<DashboardContext.Provider value={{ user, sidebarOpen, setSidebarOpen }}>
<div className="flex h-screen">
<aside className={`bg-gray-900 text-white ${sidebarOpen ? "w-64" : "w-16"} transition-all duration-300`}>
<div className="p-4 flex items-center gap-3">
<div className="w-8 h-8 bg-blue-500 rounded-full flex items-center justify-center text-sm">
{user.name[0]}
</div>
{sidebarOpen && <span className="font-bold">TaskFlow</span>}
</div>
<nav className="mt-4">
<Link href="/dashboard" className="flex items-center gap-3 p-3 hover:bg-gray-800">
<span>📊</span>
{sidebarOpen && <span>Dashboard</span>}
</Link>
<Link href="/dashboard/projects" className="flex items-center gap-3 p-3 hover:bg-gray-800">
<span>📁</span>
{sidebarOpen && <span>Projetos</span>}
</Link>
<Link href="/dashboard/team" className="flex items-center gap-3 p-3 hover:bg-gray-800">
<span>👥</span>
{sidebarOpen && <span>Equipe</span>}
</Link>
<Link href="/dashboard/settings" className="flex items-center gap-3 p-3 hover:bg-gray-800">
<span>⚙️</span>
{sidebarOpen && <span>Configurações</span>}
</Link>
</nav>
<button
onClick={() => setSidebarOpen(!sidebarOpen)}
className="absolute bottom-4 left-4 text-gray-400 hover:text-white"
>
{sidebarOpen ? "◀" : "▶"}
</button>
</aside>
<div className="flex-1 flex flex-col">
<header className="bg-white shadow-sm px-8 py-4 flex items-center justify-between">
<h2 className="text-lg font-semibold text-gray-700">
Bem-vinda, {user.name}
</h2>
<div className="flex items-center gap-4">
<button className="text-gray-500">🔔</button>
<div className="w-8 h-8 bg-gray-300 rounded-full" />
</div>
</header>
<main className="flex-1 p-8 overflow-auto">{children}</main>
</div>
</div>
</DashboardContext.Provider>
);
}
// src/app/(dashboard)/dashboard/page.tsx — Página Inicial do Dashboard
'use client';
import { useDashboard } from "../layout";
export default function DashboardHomePage() {
const { user } = useDashboard();
return (
<div>
<h1 className="text-3xl font-bold">Visão Geral do Dashboard</h1>
<p className="text-gray-500 mt-2">
Bem-vinda de volta, {user.name}! Aqui está o resumo dos seus projetos.
</p>
<div className="grid grid-cols-3 gap-6 mt-8">
<div className="bg-white p-6 rounded-xl shadow-sm">
<div className="text-sm text-gray-500">Projetos Ativos</div>
<div className="text-3xl font-bold mt-2">12</div>
</div>
<div className="bg-white p-6 rounded-xl shadow-sm">
<div className="text-sm text-gray-500">Tarefas Pendentes</div>
<div className="text-3xl font-bold mt-2">48</div>
</div>
<div className="bg-white p-6 rounded-xl shadow-sm">
<div className="text-sm text-gray-500">Membros da Equipe</div>
<div className="text-3xl font-bold mt-2">8</div>
</div>
</div>
</div>
);
}
Saída Esperada:
Acesse /login:
- Fundo em tela cheia com gradiente azul escuro
- Cartão de Formulário Branco Centralizado
- Logotipo TaskFlow + Formulário de Login
Acesse /dashboard:
- Barra lateral escura à esquerda (Dobrável, Mantém Estado Durante a Navegação)
- Barra de Navegação Superior (Mostra Bem-vinda, Alice + Ícone de Notificação)
- Área de conteúdo à direita (Visão Geral do Dashboard + 3 Cartões de Estatísticas)
- Os dados do usuário são transmitidos via Context Compartilhado
❓ Perguntas Frequentes
P: Posso usar 'use client' no layout.tsx? R: Sim. layout.tsx é um Server Component por padrão, mas se você precisar usar useState, useEffect ou manipuladores de eventos, pode adicionar a diretiva 'use client'. Nota: Uma vez adicionada, este layout e todos os seus componentes filhos se tornam Client Components.
P: Como os Grupos de Rotas afetam as URLs? R: De forma alguma. A URL para
(auth)/login/page.tsxpermanece/login;(auth)afeta apenas a estrutura do layout e não gera segmentos de caminho na URL. Este é o propósito central dos Grupos de Rotas.
P: Layout e template podem coexistir? R: Sim. Se ambos layout.tsx e template.tsx estiverem presentes no mesmo diretório, o template será envolvido dentro do layout. Quando o usuário navega, o layout permanece e o template é reconstruído.
P: A tag
<html>no layout raiz pode ser modificada? R: Sim, e você deve escrevê-la manualmente. O layout raiz substitui automaticamente o wrapper HTML padrão do Next.js, então você está livre para adicionar atributos comolang,dir,classNamee assim por diante. Isso é essencial para o suporte a RTL.
P: Qual é o número máximo de níveis de layout aninhado? R: Não há um limite estrito, mas recomendamos não exceder 3–4 níveis. Cada nível de layout cria uma
divwrapper adicional no DOM, e muitos níveis aninhados podem impactar a performance e a manutenibilidade.
P: Como os dados podem ser compartilhados entre diferentes Grupos de Rotas? R: Através do Context Provider no layout raiz. Ao envolver
UserProvider,ThemeProvidere assim por diante no layout raiz, as páginas dentro de todos os Grupos de Rotas podem acessá-los. Este é o método de compartilhamento global de dados recomendado pelo Next.js.
📖 Resumo
- Layouts aninhados permanecem montados durante a navegação, e o estado React é preservado
- Grupos de Rotas
(group)não geram segmentos de caminho na URL; são usados exclusivamente para agrupamento lógico e isolamento de layout - Layout é mais adequado para UIs persistentes, enquanto template é mais adequado para animações e atualizações de dados
- O layout raiz serve como a estrutura para todas as páginas e deve incluir
<html>,<body>e metadados - Um Context Provider é a maneira recomendada de compartilhar dados entre layouts
- Caso de Uso Mais Comum para Grupos de Rotas: Separar os layouts da página de login (sem navegação) e do painel admin (com navegação)
- O layout usa o Server Component por padrão; adicione 'use client' quando for necessária interação
📝 Exercícios
-
Exercício Básico (⭐): Crie dois layouts de Grupo de Rotas,
(marketing)/layout.tsxe(app)/layout.tsx, no projeto, e aplique estilos diferentes às páginas de marketing (Início, Sobre) e às páginas do aplicativo (Dashboard, Configurações), respectivamente. -
Exercício Avançado (⭐⭐): Coloque tanto
layout.tsxquantotemplate.tsxno mesmo diretório. AdicioneuseEffect(() => { console.log('montado') }, [])em cada arquivo, depois navegue pelas páginas e observe a saída do console para verificar as diferenças na forma como são montados. -
Desafio (⭐⭐⭐): Crie um UserProvider (Context) no layout raiz e use
useUser()tanto em(auth)/login/page.tsxquanto em(dashboard)/page.tsxpara ler os dados do usuário, verificando se o Context compartilha dados entre Grupos de Rotas.