Em uma SPA com React Router e autenticação via JWT, é comum existir uma separação clara entre a área pública (acessível sem login) e a área autenticada (acessível apenas após autenticação). “Layouts e composição de páginas” é a técnica de estruturar a aplicação para que cada área tenha uma moldura visual e comportamental consistente (menus, cabeçalhos, rodapés, barras laterais, breadcrumbs, containers, etc.), sem duplicar código e sem misturar responsabilidades.
Na prática, isso significa usar rotas aninhadas (nested routes) e componentes de layout que renderizam um <Outlet /> para encaixar as páginas internas.
Além do aspecto visual, layouts também ajudam a centralizar regras: na área autenticada, por exemplo, você pode concentrar a leitura do usuário logado, a checagem de permissões e a montagem do “shell” do app (sidebar, topbar, área de conteúdo). Já na área pública, você pode manter um layout mais simples, com navegação reduzida e foco em conversão (login, cadastro, recuperação de senha), evitando carregar elementos desnecessários.
O que é um layout no React Router (e por que ele resolve duplicação)
No React Router, um layout costuma ser um componente associado a uma rota “pai”. Esse componente renderiza elementos fixos (ex.: header) e um <Outlet />, que é o ponto onde as rotas “filhas” serão exibidas. Assim, você não precisa repetir o mesmo header em todas as páginas.
Do ponto de vista de composição, pense em camadas: (1) layout de área (pública/autenticada), (2) layout de seção (ex.: configurações, administração), (3) página final. Cada camada pode adicionar estrutura e comportamento, mantendo as páginas focadas no conteúdo.
Separando área pública e área autenticada com layouts
A separação mais comum é ter dois layouts principais:
Continue em nosso aplicativo
Você poderá ouvir o audiobook com a tela desligada, ganhar gratuitamente o certificado deste curso e ainda ter acesso a outros 5.000 cursos online gratuitos.
ou continue lendo abaixo...Baixar o aplicativo
- PublicLayout: usado para páginas como login, cadastro, “esqueci minha senha”, landing, termos. Geralmente tem um container centralizado, um header minimalista e, às vezes, um rodapé.
- AppLayout (AuthenticatedLayout): usado para o “miolo” do produto após login. Normalmente inclui topbar, sidebar, área de conteúdo com padding, e componentes globais como notificações.
Essa separação evita que o usuário veja elementos “de app” quando ainda não está logado e, ao mesmo tempo, impede que páginas públicas herdem estilos e scripts desnecessários.
Exemplo de PublicLayout
import { Outlet, Link } from "react-router-dom";export function PublicLayout() { return ( <div style={{ minHeight: "100vh", display: "grid", gridTemplateRows: "auto 1fr auto" }}> <header style={{ padding: 16, borderBottom: "1px solid #eee" }}> <nav style={{ display: "flex", gap: 12 }}> <Link to="/">Home</Link> <Link to="/login">Entrar</Link> </nav> </header> <main style={{ display: "grid", placeItems: "center", padding: 24 }}> <div style={{ width: "100%", maxWidth: 420 }}> <Outlet /> </div> </main> <footer style={{ padding: 16, borderTop: "1px solid #eee", fontSize: 12 }}> <span>© Sua aplicação</span> </footer> </div> );}Note que o layout não sabe qual página está sendo exibida. Ele apenas define a moldura e deixa o conteúdo entrar via <Outlet />.
Exemplo de AppLayout (área autenticada)
import { Outlet, NavLink } from "react-router-dom";export function AppLayout() { return ( <div style={{ minHeight: "100vh", display: "grid", gridTemplateColumns: "260px 1fr" }}> <aside style={{ borderRight: "1px solid #eee", padding: 16 }}> <h3 style={{ marginTop: 0 }}>Painel</h3> <nav style={{ display: "grid", gap: 8 }}> <NavLink to="/app" end>Dashboard</NavLink> <NavLink to="/app/profile">Perfil</NavLink> <NavLink to="/app/settings">Configurações</NavLink> </nav> </aside> <div style={{ display: "grid", gridTemplateRows: "auto 1fr" }}> <header style={{ padding: 16, borderBottom: "1px solid #eee" }}> <div style={{ display: "flex", justifyContent: "space-between" }}> <strong>Área autenticada</strong> <button type="button">Sair</button> </div> </header> <main style={{ padding: 24 }}> <Outlet /> </main> </div> </div> );}Esse layout é o “shell” do app.
O botão “Sair” ainda não está ligado a nenhuma ação; a lógica de logout e a proteção de rotas ficam em componentes específicos (guardas) ou hooks, para não acoplar demais o layout.
Passo a passo: montando rotas aninhadas com layouts
O objetivo é ter uma árvore de rotas onde cada área herda seu layout. Abaixo está um exemplo de configuração usando createBrowserRouter (ou equivalente), focando no conceito de composição. Ajuste os imports conforme sua base.
1) Defina as rotas públicas sob o PublicLayout
import { createBrowserRouter } from "react-router-dom";import { PublicLayout } from "./layouts/PublicLayout";import { HomePage } from "./pages/public/HomePage";import { LoginPage } from "./pages/public/LoginPage";import { ForgotPasswordPage } from "./pages/public/ForgotPasswordPage";export const router = createBrowserRouter([ { path: "/", element: <PublicLayout />, children: [ { index: true, element: <HomePage /> }, { path: "login", element: <LoginPage /> }, { path: "forgot-password", element: <ForgotPasswordPage /> } ] }]);Repare no uso de index: true para a rota inicial dentro do layout público. Isso mantém a URL “/” e renderiza a página dentro do <Outlet />.
2) Defina as rotas autenticadas sob o AppLayout
Agora adicionamos uma rota “/app” que usa o layout autenticado. As páginas internas ficam como filhas.
import { AppLayout } from "./layouts/AppLayout";import { DashboardPage } from "./pages/app/DashboardPage";import { ProfilePage } from "./pages/app/ProfilePage";import { SettingsPage } from "./pages/app/SettingsPage";export const router = createBrowserRouter([ { path: "/", element: <PublicLayout />, children: [ { index: true, element: <HomePage /> }, { path: "login", element: <LoginPage /> }, { path: "forgot-password", element: <ForgotPasswordPage /> } ] }, { path: "/app", element: <AppLayout />, children: [ { index: true, element: <DashboardPage /> }, { path: "profile", element: <ProfilePage /> }, { path: "settings", element: <SettingsPage /> } ] }]);Com isso, qualquer rota que comece com “/app” será renderizada dentro do AppLayout, mantendo sidebar e header consistentes.
3) Proteja a área autenticada sem poluir o layout
Um erro comum é colocar a verificação de autenticação dentro do AppLayout. Isso funciona, mas mistura responsabilidades: layout deveria cuidar de estrutura; proteção deveria ser um “guard”. Uma abordagem mais limpa é criar um componente de rota protegida que decide se renderiza o <Outlet /> ou redireciona para login.
import { Navigate, Outlet, useLocation } from "react-router-dom";import { useAuth } from "./auth/useAuth";export function RequireAuth() { const { isAuthenticated, isLoading } = useAuth(); const location = useLocation(); if (isLoading) return <p>Carregando...</p>; if (!isAuthenticated) { return <Navigate to="/login" replace state={{ from: location }} />; } return <Outlet />;}Agora, encaixe esse guard como uma rota intermediária: ele não é um layout visual, mas um “layout lógico” que apenas controla acesso.
export const router = createBrowserRouter([ { path: "/", element: <PublicLayout />, children: [ { index: true, element: <HomePage /> }, { path: "login", element: <LoginPage /> }, { path: "forgot-password", element: <ForgotPasswordPage /> } ] }, { element: <RequireAuth />, children: [ { path: "/app", element: <AppLayout />, children: [ { index: true, element: <DashboardPage /> }, { path: "profile", element: <ProfilePage /> }, { path: "settings", element: <SettingsPage /> } ] } ] }]);Observe que a rota do RequireAuth não precisa de path. Ela funciona como um agrupador que aplica a regra às rotas filhas.
Composição em múltiplos níveis: layout de seção dentro da área autenticada
Em aplicações reais, a área autenticada costuma ter seções com necessidades próprias. Exemplo: “Configurações” pode ter um menu lateral secundário, tabs ou breadcrumbs específicos. Em vez de repetir isso em cada página de configurações, crie um layout de seção.
Layout de seção: SettingsLayout
import { Outlet, NavLink } from "react-router-dom";export function SettingsLayout() { return ( <div style={{ display: "grid", gridTemplateColumns: "220px 1fr", gap: 16 }}> <aside style={{ borderRight: "1px solid #eee", paddingRight: 16 }}> <h3 style={{ marginTop: 0 }}>Configurações</h3> <nav style={{ display: "grid", gap: 8 }}> <NavLink to="/app/settings" end>Geral</NavLink> <NavLink to="/app/settings/security">Segurança</NavLink> <NavLink to="/app/settings/billing">Cobrança</NavLink> </nav> </aside> <section> <Outlet /> </section> </div> );}Agora, as rotas de configurações ficam aninhadas dentro de SettingsLayout.
import { SettingsLayout } from "./pages/app/settings/SettingsLayout";import { SettingsGeneralPage } from "./pages/app/settings/SettingsGeneralPage";import { SettingsSecurityPage } from "./pages/app/settings/SettingsSecurityPage";import { SettingsBillingPage } from "./pages/app/settings/SettingsBillingPage";{ path: "/app", element: <AppLayout />, children: [ { index: true, element: <DashboardPage /> }, { path: "profile", element: <ProfilePage /> }, { path: "settings", element: <SettingsLayout />, children: [ { index: true, element: <SettingsGeneralPage /> }, { path: "security", element: <SettingsSecurityPage /> }, { path: "billing", element: <SettingsBillingPage /> } ] } ]}Com isso, você tem três níveis de composição: RequireAuth, AppLayout, SettingsLayout e a página final.
Redirecionamento pós-login preservando a página de origem
Quando o usuário tenta acessar uma rota autenticada sem estar logado, o guard redireciona para /login com um state contendo a rota original. Depois do login, você pode ler esse state e voltar para onde ele queria ir. Isso melhora a experiência e evita que o usuário sempre caia no dashboard.
Exemplo de LoginPage respeitando “from”
import { useLocation, useNavigate } from "react-router-dom";import { useAuth } from "../auth/useAuth";export function LoginPage() { const navigate = useNavigate(); const location = useLocation(); const { signIn } = useAuth(); const from = location.state?.from?.pathname || "/app"; async function handleSubmit(e) { e.preventDefault(); const form = new FormData(e.currentTarget); const email = String(form.get("email")); const password = String(form.get("password")); await signIn({ email, password }); navigate(from, { replace: true }); } return ( <div> <h2>Entrar</h2> <form onSubmit={handleSubmit}> <div> <label>Email</label> <input name="email" type="email" required /> </div> <div> <label>Senha</label> <input name="password" type="password" required /> </div> <button type="submit">Entrar</button> </form> </div> );}Esse fluxo combina bem com layouts: o login fica no PublicLayout e, após autenticar, o usuário entra no AppLayout já na rota correta.
Compondo páginas com “Page Shell” e componentes reutilizáveis
Além dos layouts de rota, é útil padronizar a composição interna das páginas com componentes de estrutura. Um padrão comum é um “PageShell” que define título, ações e área de conteúdo. Isso reduz variações de espaçamento e mantém consistência.
Exemplo de PageShell
export function PageShell({ title, actions, children }) { return ( <div style={{ display: "grid", gap: 16 }}> <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}> <h2 style={{ margin: 0 }}>{title}</h2> <div>{actions}</div> </div> <div>{children}</div> </div> );}Uso em uma página autenticada:
import { PageShell } from "../../components/PageShell";export function ProfilePage() { return ( <PageShell title="Perfil" actions={<button type="button">Editar</button>} > <p>Dados do usuário...</p> </PageShell> );}Esse tipo de composição não depende do React Router, mas complementa os layouts: o layout define a moldura macro; o PageShell define a moldura micro dentro do conteúdo.
Estados globais no layout: carregamento do usuário e renderização condicional
Na área autenticada, frequentemente você precisa exibir informações do usuário (nome, avatar) no header. Uma prática importante é evitar “piscar” (flash) de UI incorreta: o layout pode renderizar um estado de carregamento enquanto o contexto de autenticação resolve se há token válido e se o perfil foi carregado.
Em vez de o AppLayout buscar dados diretamente, prefira que o estado de autenticação exponha algo como user e isLoading. O layout apenas consome e decide como apresentar.
import { Outlet } from "react-router-dom";import { useAuth } from "../auth/useAuth";export function AppLayout() { const { user } = useAuth(); return ( <div> <header style={{ padding: 16, borderBottom: "1px solid #eee" }}> <div style={{ display: "flex", justifyContent: "space-between" }}> <strong>Painel</strong> <span>{user ? user.name : ""}</span> </div> </header> <main style={{ padding: 24 }}> <Outlet /> </main> </div> );}Se o seu RequireAuth já bloqueia acesso enquanto isLoading é true, o AppLayout pode assumir que o usuário existe. Caso contrário, trate o estado com cuidado para não quebrar renderização.
Erros comuns ao criar layouts para público e autenticado (e como evitar)
- Duplicar navegação em todas as páginas: se você está copiando o mesmo header/sidebar em várias páginas, está faltando um layout com
<Outlet />. - Misturar proteção de rota com UI: colocar redirecionamento dentro do AppLayout pode funcionar, mas tende a criar loops e efeitos colaterais. Prefira um guard dedicado.
- Rotas absolutas dentro de seções: em rotas aninhadas, use caminhos relativos quando fizer sentido (dependendo do seu estilo). Para links, seja consistente: ou use caminhos absolutos (mais explícitos) ou relativos (mais fáceis de mover). Misturar sem critério confunde.
- Layout público carregando dependências da área autenticada: evite importar componentes pesados do app dentro do layout público. Isso reduz performance e aumenta acoplamento.
- Não padronizar espaçamentos: sem um PageShell (ou padrão similar), cada página inventa seu próprio espaçamento e hierarquia visual, gerando inconsistência.
Passo a passo: adicionando um layout “sem navegação” para telas especiais
Algumas telas não se encaixam bem nem no layout público padrão nem no layout autenticado completo. Exemplos: página de “reset de senha” com token, página de “aceitar convite”, ou uma tela de “sessão expirada”. Você pode criar um layout minimalista, reaproveitando o conceito de composição.
1) Crie um MinimalLayout
import { Outlet } from "react-router-dom";export function MinimalLayout() { return ( <div style={{ minHeight: "100vh", display: "grid", placeItems: "center", padding: 24 }}> <div style={{ width: "100%", maxWidth: 520 }}> <Outlet /> </div> </div> );}2) Use-o em rotas específicas
import { MinimalLayout } from "./layouts/MinimalLayout";import { ResetPasswordPage } from "./pages/public/ResetPasswordPage";export const router = createBrowserRouter([ { path: "/", element: <PublicLayout />, children: [ { index: true, element: <HomePage /> }, { path: "login", element: <LoginPage /> } ] }, { element: <MinimalLayout />, children: [ { path: "/reset-password", element: <ResetPasswordPage /> } ] }]);Esse padrão mantém o layout público “limpo” e evita exceções dentro dele.
Composição orientada a domínio: quando criar um layout por módulo
Quando a aplicação cresce, você pode ter módulos com navegação e identidade próprias (ex.: “Administração”, “Relatórios”, “Suporte”). Em vez de colocar tudo no AppLayout, crie layouts de módulo aninhados. Isso reduz a complexidade do AppLayout e torna cada módulo mais independente.
Exemplo: um AdminLayout pode adicionar um submenu, uma barra de filtros ou até um tema visual diferente, sem afetar o restante do app. A regra prática é: se um conjunto de páginas compartilha estrutura e comportamento, isso é um candidato a layout.
Checklist prático para validar seus layouts
- As páginas públicas não exibem sidebar/topbar do app autenticado.
- As páginas autenticadas compartilham navegação e estrutura sem duplicação.
- A proteção de rotas está centralizada em um guard e não espalhada por páginas.
- Existe um padrão de composição interna (ex.: PageShell) para títulos e ações.
- Rotas de seção (ex.: settings) têm layout próprio quando há elementos compartilhados.
- O redirecionamento pós-login retorna o usuário para a rota original quando aplicável.