Em uma SPA, “rotas públicas” são páginas acessíveis sem autenticação (ex.: login, cadastro, recuperação de senha, landing), enquanto “rotas privadas” exigem que o usuário esteja autenticado (ex.: dashboard, perfil, faturamento). Para implementar isso de forma consistente, usamos componentes de guarda (guards): componentes que ficam entre a navegação e o conteúdo e decidem se a rota pode ser acessada, redirecionando quando necessário.
O objetivo de um guard não é “proteger” dados sensíveis (isso é responsabilidade do backend), e sim controlar a experiência de navegação no cliente: impedir que usuários não autenticados vejam telas internas, evitar que usuários autenticados voltem ao login, lidar com estados intermediários (carregando sessão), e aplicar regras como permissões/roles.
Conceitos essenciais: público, privado e estados de autenticação
Antes de escrever guards, vale definir claramente os estados possíveis do “status de autenticação” no frontend. Em SPAs reais, não existe apenas “logado” e “deslogado”. Normalmente você precisa de pelo menos três estados:
- loading: ainda estamos verificando se existe sessão válida (ex.: lendo token do storage, validando expiração, buscando /me).
- authenticated: há um usuário autenticado e pronto para acessar áreas privadas.
- unauthenticated: não há sessão válida; o usuário deve ficar na área pública.
Esses estados são importantes porque um guard que redireciona imediatamente ao ver “sem usuário” pode causar “flicker” (piscar) ou redirecionamentos errados enquanto a sessão ainda está sendo carregada.
O que é um Guard no React Router
Um guard é um componente que envolve uma rota (ou um conjunto de rotas) e decide o que renderizar:
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

- Se permitido: renderiza o conteúdo da rota via
<Outlet />(rotas aninhadas) ou renderizachildren. - Se não permitido: redireciona usando
<Navigate />ou dispara uma navegação programática. - Se em carregamento: mostra um placeholder (skeleton/spinner) para evitar redirecionamento prematuro.
Na prática, você terá pelo menos dois guards comuns:
- AuthGuard (rota privada): exige autenticação.
- GuestGuard (rota pública restrita): impede acesso a login/cadastro quando já está autenticado.
Passo a passo: criando uma fonte única de verdade para a sessão
Os guards precisam consultar um estado central de autenticação. A implementação pode variar (Context API, Zustand, Redux, React Query), mas o ponto é: o guard não deve “adivinhar” sozinho; ele deve consumir um estado já preparado.
1) Defina um contrato de estado de autenticação
Um formato simples e útil:
status:'loading' | 'authenticated' | 'unauthenticated'user: objeto do usuário ounulltoken: string do JWT (se você mantiver no cliente) ounullhasRole(role)/hasPermission(permission): helpers opcionais
Exemplo de tipagem (TypeScript) para guiar o raciocínio:
type AuthStatus = 'loading' | 'authenticated' | 'unauthenticated';
type User = {
id: string;
name: string;
email: string;
roles?: string[];
};
type AuthState = {
status: AuthStatus;
user: User | null;
token: string | null;
};2) Crie um hook de acesso ao estado (exemplo com Context)
O guard vai consumir algo como useAuth(). Abaixo está um exemplo mínimo de interface; a forma de preencher esse estado (login, logout, refresh) pode já existir no seu projeto, então foque no consumo.
import React from 'react';
type AuthContextValue = {
status: 'loading' | 'authenticated' | 'unauthenticated';
user: { id: string; name: string; roles?: string[] } | null;
};
const AuthContext = React.createContext<AuthContextValue | null>(null);
export function useAuth() {
const ctx = React.useContext(AuthContext);
if (!ctx) throw new Error('useAuth deve ser usado dentro de AuthProvider');
return ctx;
}O ponto aqui é: o guard não acessa diretamente localStorage nem faz fetch por conta própria. Ele apenas reage ao estado.
Passo a passo: implementando um AuthGuard (rotas privadas)
O AuthGuard deve:
- Enquanto
status === 'loading': renderizar um fallback (ex.: “Carregando…”). - Se
status === 'unauthenticated': redirecionar para a rota de login. - Se
status === 'authenticated': permitir acesso e renderizar o conteúdo.
Além disso, é uma boa prática preservar a rota original para voltar após login (deep link). Isso é feito passando state no <Navigate />.
import { Navigate, Outlet, useLocation } from 'react-router-dom';
import { useAuth } from '../auth/useAuth';
export function AuthGuard() {
const { status } = useAuth();
const location = useLocation();
if (status === 'loading') {
return <div>Carregando sessão...</div>;
}
if (status === 'unauthenticated') {
return (
<Navigate
to="/login"
replace
state={{ from: location }}
/>
);
}
return <Outlet />;
}Por que usar replace? Para evitar que o usuário volte com “voltar” do navegador para uma rota privada que imediatamente redirecionaria de novo, criando uma navegação confusa.
Aplicando o AuthGuard em rotas aninhadas
O padrão mais limpo é agrupar rotas privadas sob um mesmo guard:
import { Routes, Route } from 'react-router-dom';
import { AuthGuard } from './guards/AuthGuard';
export function AppRoutes() {
return (
<Routes>
<Route path="/" element={<Home />} />
<Route path="/login" element={<Login />} />
<Route element={<AuthGuard />}>
<Route path="/app" element={<Dashboard />} />
<Route path="/app/profile" element={<Profile />} />
<Route path="/app/billing" element={<Billing />} />
</Route>
<Route path="*" element={<NotFound />} />
</Routes>
);
}Note que o guard está em um <Route element={...}> sem path. Isso cria um “grupo” de rotas que compartilham a mesma regra de acesso.
Passo a passo: implementando um GuestGuard (rotas públicas que não fazem sentido para logados)
Algumas rotas são públicas, mas não deveriam ser acessadas por quem já está autenticado, como /login e /register. Para isso, crie um guard inverso:
- Se
loading: renderiza fallback. - Se
authenticated: redireciona para a área autenticada (ex.:/app). - Se
unauthenticated: permite renderizar a página pública.
import { Navigate, Outlet } from 'react-router-dom';
import { useAuth } from '../auth/useAuth';
export function GuestGuard() {
const { status } = useAuth();
if (status === 'loading') {
return <div>Carregando...</div>;
}
if (status === 'authenticated') {
return <Navigate to="/app" replace />;
}
return <Outlet />;
}Uso típico:
<Routes>
<Route element={<GuestGuard />}>
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
</Route>
<Route element={<AuthGuard />}>
<Route path="/app" element={<Dashboard />} />
</Route>
</Routes>Redirecionamento pós-login preservando a rota original
Quando o AuthGuard redireciona para /login com state={{ from: location }}, a tela de login pode ler esse estado e, após autenticar, navegar de volta para a rota original.
import { useLocation, useNavigate } from 'react-router-dom';
export function Login() {
const navigate = useNavigate();
const location = useLocation();
const from = (location.state as any)?.from?.pathname || '/app';
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
// await signIn(...)
navigate(from, { replace: true });
}
return (
<form onSubmit={handleSubmit}>
<button type="submit">Entrar</button>
</form>
);
}Esse detalhe melhora muito a experiência: se o usuário acessa um link direto para /app/billing, ele faz login e volta exatamente para /app/billing.

Guard de autorização: roles e permissões
Autenticação responde “quem é você?”. Autorização responde “o que você pode fazer?”. Em muitas SPAs, além de exigir login, você precisa restringir páginas por perfil (ex.: admin) ou por permissão (ex.: billing:read).
Uma abordagem prática é criar um guard específico, ou estender o AuthGuard com parâmetros. Um guard dedicado deixa as regras mais explícitas.
Exemplo: RoleGuard
Esse guard exige autenticação e uma role específica. Se não tiver a role, você pode redirecionar para uma página de “sem permissão” (403) ou para um fallback seguro.
import { Navigate, Outlet } from 'react-router-dom';
import { useAuth } from '../auth/useAuth';
type RoleGuardProps = {
requiredRole: string;
};
export function RoleGuard({ requiredRole }: RoleGuardProps) {
const { status, user } = useAuth();
if (status === 'loading') return <div>Carregando...</div>;
if (status === 'unauthenticated') {
return <Navigate to="/login" replace />;
}
const roles = user?.roles ?? [];
const allowed = roles.includes(requiredRole);
if (!allowed) {
return <Navigate to="/forbidden" replace />;
}
return <Outlet />;
}Uso:
<Route element={<AuthGuard />}>
<Route path="/app" element={<Dashboard />} />
<Route element={<RoleGuard requiredRole="admin" />}>
<Route path="/app/admin" element={<AdminPanel />} />
</Route>
</Route>Repare que você pode compor guards: primeiro garante autenticação, depois aplica autorização. Isso evita duplicar lógica.
Guard por “feature flag” ou estado do usuário (ex.: onboarding completo)
Nem toda regra é role/permissão. Às vezes você quer bloquear páginas até o usuário completar um passo (ex.: aceitar termos, completar perfil, configurar 2FA). Isso também é um guard.
Exemplo: exigir que o usuário tenha onboardingCompleted antes de acessar o app principal.

import { Navigate, Outlet } from 'react-router-dom';
import { useAuth } from '../auth/useAuth';
export function OnboardingGuard() {
const { status, user } = useAuth();
if (status === 'loading') return <div>Carregando...</div>;
if (status === 'unauthenticated') return <Navigate to="/login" replace />;
const completed = (user as any)?.onboardingCompleted;
if (!completed) return <Navigate to="/onboarding" replace />;
return <Outlet />;
}Isso ajuda a manter regras de fluxo fora das páginas, evitando condicionais espalhadas.
Erros comuns e como evitar
1) Redirecionar durante o carregamento da sessão
Se o guard não tratar loading, o usuário pode ser mandado ao login mesmo tendo token válido, apenas porque o estado ainda não foi hidratado. Sempre trate o estado intermediário.
2) Guard lendo token diretamente do storage
Quando cada guard lê localStorage ou decodifica JWT por conta própria, você cria divergência de regras e bugs difíceis (ex.: um guard considera válido, outro não). Centralize a lógica de sessão em um único lugar e exponha um estado simples para os guards.
3) Confundir “esconder tela” com segurança real
Mesmo com AuthGuard, um atacante pode chamar APIs diretamente. O backend deve validar JWT e permissões em todas as rotas protegidas. No frontend, o guard é uma camada de UX e organização.
4) Loops de redirecionamento
Loops acontecem quando:
- Você redireciona para uma rota que também está sob o mesmo guard.
- Você manda para
/login, mas/loginestá dentro de AuthGuard por engano. - Você usa GuestGuard e AuthGuard com destinos que se apontam mutuamente em certos estados.
Para evitar, mantenha uma separação clara: rotas públicas em um grupo, rotas privadas em outro, e uma rota de fallback (ex.: /forbidden) fora de guards que possam bloquear seu acesso.
Padrão prático: um único componente “ProtectedRoute” com children
Se você preferir proteger rotas individualmente (em vez de grupos com Outlet), pode criar um componente que recebe children. Isso é útil em cenários onde você monta rotas dinamicamente ou quer proteger apenas um elemento.
import { Navigate, useLocation } from 'react-router-dom';
import { useAuth } from '../auth/useAuth';
type ProtectedRouteProps = {
children: React.ReactNode;
};
export function ProtectedRoute({ children }: ProtectedRouteProps) {
const { status } = useAuth();
const location = useLocation();
if (status === 'loading') return <div>Carregando...</div>;
if (status === 'unauthenticated') {
return <Navigate to="/login" replace state={{ from: location }} />;
}
return <>{children}</>;
}Uso:
<Route
path="/app/profile"
element={
<ProtectedRoute>
<Profile />
</ProtectedRoute>
}
/>Esse padrão é simples, mas para muitas rotas ele fica verboso. Para áreas inteiras, o padrão com Outlet costuma ser mais escalável.
Tratando expiração de token e sessão inválida durante a navegação
Mesmo com guards, a sessão pode expirar enquanto o usuário está navegando. O que o guard deve fazer nesse caso depende de como seu estado de autenticação é atualizado. O importante é que, ao detectar “sessão inválida”, o estado central mude para unauthenticated. Assim, qualquer rota privada renderizada sob AuthGuard será automaticamente redirecionada.
Na prática, isso costuma acontecer quando:
- Uma chamada a API retorna 401/403 e você decide deslogar.
- Você detecta expiração do JWT (ex.: claim
exp) e invalida a sessão.
O guard não precisa conhecer detalhes de JWT; ele apenas reage ao estado. Isso mantém o controle de acesso previsível.
Checklist de implementação para controle de acesso com guards
- Defina estados
loading/authenticated/unauthenticatede garanta que o app inicia emloadingaté resolver a sessão. - Implemente
AuthGuardcomOutlete preservefrompara pós-login. - Implemente
GuestGuardpara evitar login/cadastro para usuários autenticados. - Quando necessário, componha guards de autorização (roles/permissões) com rotas aninhadas.
- Garanta que rotas de erro como
/forbiddene/not-foundnão causem loops. - Centralize a lógica de sessão; guards apenas consomem estado.