Capa do Ebook gratuito Roteamento e Autenticação em React: Construindo SPAs Seguras com React Router e JWT

Roteamento e Autenticação em React: Construindo SPAs Seguras com React Router e JWT

Novo curso

20 páginas

Layouts e composição de páginas para área pública e área autenticada

Capítulo 4

Tempo estimado de leitura: 11 minutos

+ Exercício
Audio Icon

Ouça em áudio

0:00 / 0:00

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...
Download App

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.

Agora responda o exercício sobre o conteúdo:

Qual é a principal vantagem de usar rotas aninhadas com um componente de layout que renderiza um ao separar área pública e área autenticada em React Router?

Você acertou! Parabéns, agora siga para a próxima página

Você errou! Tente novamente.

O layout com Outlet cria uma moldura fixa (header, sidebar, containers) e encaixa as rotas filhas dentro dela, reduzindo duplicação. A proteção de rotas deve ficar em um guard dedicado, não no layout.

Próximo capitúlo

Rotas públicas, rotas privadas e componentes de guarda (Guards) para controle de acesso

Arrow Right Icon
Baixe o app para ganhar Certificação grátis e ouvir os cursos em background, mesmo com a tela desligada.