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

Páginas de erro e estados de acesso: 404, 403 e fallbacks de carregamento

Capítulo 15

Tempo estimado de leitura: 10 minutos

+ Exercício
Audio Icon

Ouça em áudio

0:00 / 0:00

Numa SPA, o usuário navega sem recarregar a página inteira. Isso torna a experiência rápida, mas também exige que você modele explicitamente o que acontece quando uma rota não existe (404), quando existe mas o usuário não tem permissão (403) e quando a aplicação ainda está carregando dados ou código (fallbacks de carregamento). Esses estados não são “detalhes de UI”: eles afetam segurança percebida, clareza de navegação, suporte e até métricas de conversão.

Entendendo 404, 403 e estados de fallback

404 (Not Found) em SPAs

O 404 é o estado em que a URL não corresponde a nenhuma rota conhecida pela aplicação. Em React Router, isso normalmente é resolvido com uma rota “catch-all” (curinga) que renderiza uma página de não encontrado. Em SPAs, há dois tipos de 404 para considerar:

  • 404 de roteamento no cliente: a aplicação carregou, mas nenhuma rota bateu. Você controla totalmente a UI.

  • 404 do servidor: o usuário acessa diretamente uma URL (ex.: /app/relatorios) e o servidor não está configurado para servir o index.html para rotas do cliente. Esse é um problema de deploy/configuração, mas a página 404 do cliente não será exibida porque o app nem carregou.

Neste capítulo, o foco é o 404 no cliente (React Router). Ainda assim, é importante lembrar que, em produção, você deve configurar o servidor para “fallback para index.html” em rotas da SPA, e deixar 404 real apenas para assets inexistentes.

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

403 (Forbidden) e a diferença para 401

O 403 representa “você está autenticado (ou identificado), mas não tem permissão para acessar este recurso”. Em SPAs, 403 costuma aparecer em dois cenários:

  • Bloqueio por regra de UI/rota: você sabe que o usuário não tem permissão (por exemplo, role insuficiente) e decide renderizar uma página 403.

  • Resposta do backend: a API retorna 403 ao tentar acessar um endpoint. A UI precisa reagir e mostrar uma tela adequada ou um aviso contextual.

Já o 401 (Unauthorized) normalmente significa “não autenticado” (sem sessão válida). Muitos projetos tratam 401 redirecionando para login, enquanto 403 mantém o usuário logado e exibe “Acesso negado”. Como capítulos anteriores já cobriram guards, roles e interceptação de erros, aqui vamos focar em como transformar essas condições em páginas e fluxos de navegação consistentes.

Fallbacks de carregamento

Fallback é o que a UI mostra enquanto algo ainda não está pronto: carregamento de código (lazy loading), carregamento de dados (fetch), ou resolução de estado (por exemplo, restaurar sessão). Em React Router moderno, você pode lidar com isso de várias formas:

  • React.Suspense para carregamento de módulos via lazy().

  • Data routers (loaders) com defer/Await (quando aplicável) para carregamento de dados.

  • Estados locais (skeleton/spinner) em páginas e componentes.

O objetivo é evitar telas em branco, evitar “piscadas” de layout e comunicar claramente o que está acontecendo.

Estratégia prática: padronizar páginas de erro e carregamento

Antes do passo a passo, defina um padrão simples:

  • NotFound (404): rota inexistente; oferecer ação de voltar para home/área inicial e, opcionalmente, um link para suporte.

  • Forbidden (403): rota existente, mas acesso negado; oferecer ação de voltar para uma área permitida e, opcionalmente, trocar de conta.

  • Loading fallback: componente reutilizável (ex.: <PageLoader />) e, quando possível, skeletons específicos por página.

  • Error boundary de rota: capturar erros inesperados de renderização/loader e mostrar uma página “Algo deu errado” sem derrubar a SPA inteira.

Mesmo que você não implemente todas as camadas de uma vez, ter nomes e responsabilidades claras evita duplicação e inconsistência.

Passo a passo: criando páginas 404 e 403

1) Criar componentes de página

Crie páginas simples e reutilizáveis. Evite colocar lógica de permissão dentro delas; elas devem apenas apresentar o estado e ações de navegação.

import { Link, useNavigate } from "react-router-dom";export function NotFoundPage() {  const navigate = useNavigate();  return (    <div style={{ padding: 24 }}>      <h2>Página não encontrada (404)</h2>      <p>A rota acessada não existe ou foi movida.</p>      <ul>        <li><button onClick={() => navigate(-1)}>Voltar</button></li>        <li><Link to="/">Ir para a página inicial</Link></li>      </ul>    </div>  );}export function ForbiddenPage() {  return (    <div style={{ padding: 24 }}>      <h2>Acesso negado (403)</h2>      <p>Você não tem permissão para acessar esta página.</p>      <ul>        <li><Link to="/">Ir para a página inicial</Link></li>        <li><Link to="/app">Ir para a área autenticada</Link></li>      </ul>    </div>  );}

Observação: os links acima são exemplos. Ajuste para as rotas reais do seu app (por exemplo, uma dashboard padrão).

2) Registrar a rota 404 (catch-all)

No React Router, a rota curinga * deve ficar por último dentro do mesmo nível de rotas, para capturar qualquer caminho não mapeado.

import { Routes, Route } from "react-router-dom";import { NotFoundPage } from "./pages/NotFoundPage";export function AppRoutes() {  return (    <Routes>      {/* ...suas rotas... */}      <Route path="*" element={<NotFoundPage />} />    </Routes>  );}

Se você tem rotas aninhadas (layouts), também pode querer um 404 “local” dentro de um segmento. Exemplo: rotas dentro de /app podem ter um 404 específico para a área autenticada, mantendo o layout (menu, topo, etc.).

<Route path="/app" element={<AppLayout />}>  {/* ...rotas internas... */}  <Route path="*" element={<NotFoundPage />} /></Route>

Isso evita que um usuário caia num 404 “público” sem o contexto do app quando ele erra uma sub-rota dentro da área autenticada.

3) Registrar a rota 403 (página dedicada)

Para 403, é comum ter uma rota explícita (ex.: /403 ou /forbidden). Isso permite redirecionar para ela quando uma verificação de permissão falhar.

<Route path="/forbidden" element={<ForbiddenPage />} />

Mesmo que você também possa renderizar <ForbiddenPage /> diretamente no lugar da página protegida, a rota dedicada facilita padronizar navegação, breadcrumbs e telemetria (ex.: medir quantas vezes usuários chegam ao 403).

Passo a passo: quando mostrar 403 (sem repetir guards)

Como a lógica de guards e roles já foi tratada anteriormente, aqui vamos focar em padrões de saída: o que fazer quando a permissão falha.

Padrão A: renderizar 403 no lugar do conteúdo

Esse padrão mantém a URL original. É útil quando você quer que o usuário entenda exatamente qual página tentou acessar, mas não pode.

import { ForbiddenPage } from "./pages/ForbiddenPage";export function ReportsPage() {  const canView = false; // resultado de uma checagem já existente no seu app  if (!canView) return <ForbiddenPage />;  return (    <div>      <h2>Relatórios</h2>      <p>Conteúdo permitido...</p>    </div>  );}

Cuidados: se você tem navegação lateral destacando a rota atual, ela continuará ativa. Isso pode ser desejável (o usuário “está” naquela área) ou confuso (parece que a página existe, mas está bloqueada). Avalie caso a caso.

Padrão B: redirecionar para uma rota /forbidden

Esse padrão troca a URL para uma página padrão de acesso negado. É útil quando você quer centralizar UI e reduzir variações.

import { Navigate, useLocation } from "react-router-dom";export function RequirePermission({ allowed, children }) {  const location = useLocation();  if (!allowed) {    return <Navigate to="/forbidden" replace state={{ from: location }} />;  }  return children;}

O state.from permite exibir na página 403 de onde o usuário veio (ou oferecer um botão “voltar”), sem expor detalhes sensíveis. Na página 403, você pode ler esse estado e decidir a melhor ação.

Fallbacks de carregamento: evitando telas em branco

1) Fallback para lazy loading com Suspense

Quando você divide o bundle e carrega páginas sob demanda, o React precisa de um fallback enquanto o chunk é baixado. Um padrão é envolver o “miolo” das rotas com <Suspense>.

import { Suspense, lazy } from "react";import { Routes, Route } from "react-router-dom";const SettingsPage = lazy(() => import("./pages/SettingsPage"));function PageLoader() {  return (    <div style={{ padding: 24 }}>      <p>Carregando...</p>    </div>  );}export function AppRoutes() {  return (    <Suspense fallback={<PageLoader />}>      <Routes>        <Route path="/app/settings" element={<SettingsPage />} />        <Route path="*" element={<NotFoundPage />} />      </Routes>    </Suspense>  );}

Dica prática: se você tem layouts diferentes (público vs autenticado), pode colocar um Suspense por layout para que o fallback mantenha a moldura (header/sidebar) e só o conteúdo central mostre carregamento.

2) Fallback de carregamento de dados dentro da página

Mesmo sem data routers, você frequentemente terá páginas que buscam dados. O padrão é: estado loading, estado error, estado empty e estado success. O erro aqui não é 403/404 de rota, mas falhas de rede, 500, timeouts etc. Ainda assim, um fallback consistente melhora muito a UX.

import { useEffect, useState } from "react";function InlineError({ message }) {  return <p style={{ color: "crimson" }}>{message}</p>;}export function ProfilePage() {  const [loading, setLoading] = useState(true);  const [error, setError] = useState(null);  const [profile, setProfile] = useState(null);  useEffect(() => {    let alive = true;    (async () => {      try {        setLoading(true);        setError(null);        const res = await fetch("/api/me");        if (!res.ok) {          if (res.status === 403) throw new Error("forbidden");          throw new Error("request_failed");        }        const data = await res.json();        if (alive) setProfile(data);      } catch (e) {        if (!alive) return;        setError(e.message);      } finally {        if (alive) setLoading(false);      }    })();    return () => { alive = false; };  }, []);  if (loading) return <p>Carregando perfil...</p>;  if (error === "forbidden") return <ForbiddenPage />;  if (error) return <InlineError message="Não foi possível carregar." />;  if (!profile) return <p>Sem dados.</p>;  return (    <div>      <h2>Meu perfil</h2>      <p>Nome: {profile.name}</p>    </div>  );}

Note que, quando o backend devolve 403, você pode optar por renderizar a página 403 mesmo dentro de uma rota válida. Isso é útil quando a permissão depende de dados do servidor (por exemplo, feature flags, assinatura expirada, bloqueio administrativo).

3) Skeletons e estabilidade visual

Um loader genérico é melhor do que tela em branco, mas skeletons melhoram a percepção de velocidade.

A regra prática:

  • Use spinner/loader para esperas curtas e ações pontuais (ex.: salvar).

  • Use skeleton para páginas com layout previsível (ex.: cards, tabelas).

  • Mantenha o layout estável: reserve espaço para cabeçalhos, cards e tabelas para evitar “pulo” de conteúdo.

Mesmo sem biblioteca, você pode criar um skeleton simples com CSS e blocos.

export function TableSkeleton() {  return (    <div style={{ padding: 24 }}>      <div style={{ height: 24, width: 220, background: "#eee", marginBottom: 16 }} />      <div style={{ height: 14, width: "100%", background: "#eee", marginBottom: 8 }} />      <div style={{ height: 14, width: "100%", background: "#eee", marginBottom: 8 }} />      <div style={{ height: 14, width: "100%", background: "#eee", marginBottom: 8 }} />    </div>  );}

Erros de rota além de 404/403: errorElement e boundaries

Além de “rota não encontrada” e “acesso negado”, existem erros inesperados: exceções durante renderização, falhas em loaders (se você usa data routers), ou bugs de componentes. Em vez de quebrar a SPA inteira, você pode definir um errorElement por segmento de rota para capturar e exibir uma tela de erro contextual.

Exemplo conceitual com rotas que suportam errorElement:

import { createBrowserRouter } from "react-router-dom";function RouteErrorPage() {  return (    <div style={{ padding: 24 }}>      <h2>Ocorreu um erro</h2>      <p>Tente recarregar a página ou voltar.</p>    </div>  );}export const router = createBrowserRouter([  {    path: "/app",    element: <AppLayout />,    errorElement: <RouteErrorPage />,    children: [      { path: "settings", element: <SettingsPage /> },      { path: "*", element: <NotFoundPage /> }    ]  },  { path: "/forbidden", element: <ForbiddenPage /> },  { path: "*", element: <NotFoundPage /> }]);

Esse padrão é especialmente útil para não confundir erros de aplicação com 404. Se um componente quebra, não faz sentido mostrar “Página não encontrada”; faz mais sentido mostrar “Erro ao carregar esta tela”.

Integração com respostas do backend: 404 e 403 de recurso

Nem todo 404 vem do roteamento. Às vezes a rota existe, mas o recurso não: por exemplo, /app/users/123 existe, mas o usuário 123 não. Nesse caso, o backend pode retornar 404 e você deve mostrar um estado de “recurso não encontrado” dentro da página (ou uma página dedicada).

Um padrão simples é separar:

  • 404 de rota: URL inválida na navegação do app → NotFoundPage via rota *.

  • 404 de recurso: entidade inexistente → ResourceNotFound dentro da página, mantendo breadcrumbs e contexto.

function ResourceNotFound({ entityName }) {  return (    <div style={{ padding: 24 }}>      <h3>{entityName} não encontrado</h3>      <p>Verifique se o link está correto ou se o item foi removido.</p>    </div>  );}

Da mesma forma, um 403 pode ser “por rota” (regra conhecida no cliente) ou “por recurso” (o usuário não pode ver aquele item específico). A UI pode variar: em alguns casos, é melhor mostrar 404 para não revelar a existência do recurso (decisão de segurança do backend). No front-end, respeite o código e a mensagem que a API retorna, mas mantenha consistência visual.

Checklist de implementação (para evitar armadilhas comuns)

  • Catch-all no nível correto: tenha * global e, se necessário, * dentro de segmentos como /app.

  • 403 não é 404: não use 404 para tudo; diferencie acesso negado de rota inexistente.

  • Fallback sempre visível: envolva rotas lazy com Suspense e garanta que o fallback não “quebre” o layout.

  • Evite loops de navegação: ao redirecionar para /forbidden, use replace para não poluir histórico e evitar voltar para a mesma rota bloqueada repetidamente.

  • Mensagens acionáveis: ofereça caminhos claros (voltar, ir para home, ir para dashboard) em 404 e 403.

  • Telemetria: registre eventos de 404/403 (por exemplo, em analytics/logs) para identificar links quebrados e problemas de permissão.

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

Em uma SPA com React Router, qual abordagem melhor diferencia uma rota inexistente de um acesso negado, mantendo navegação consistente?

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

Você errou! Tente novamente.

O 404 trata URL sem rota conhecida (geralmente via catch-all *), enquanto o 403 indica rota existente com acesso negado. Separar NotFound e Forbidden (ex.: /forbidden) melhora clareza e consistência de navegação.

Próximo capitúlo

Boas práticas de segurança no front-end: XSS, CSRF com cookies e hardening de UI

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