Em uma SPA, o usuário navega entre rotas sem recarregar a página. Quando uma rota exige autenticação e o usuário ainda não está logado, você normalmente o redireciona para a tela de login. O problema prático surge logo em seguida: após autenticar, para onde o usuário deve ir? Se você sempre mandar para um “/dashboard” fixo, você quebra a expectativa de “voltar para onde eu estava indo”. É aí que entra o conceito de preservação da rota pretendida, geralmente chamada de returnUrl (ou redirectTo, from, next).
Neste capítulo, você vai implementar redirecionamentos pós-login com preservação da rota pretendida, cobrindo: como capturar a rota original, como passá-la com segurança para a tela de login, como restaurá-la após o login e como lidar com casos especiais (query string, hash, deep links, rotas com estado, e cenários de logout/expiração).
O que é “returnUrl” e por que ele existe
returnUrl é um valor que representa a rota que o usuário tentou acessar antes de ser redirecionado ao login. Após o login bem-sucedido, a aplicação usa esse valor para navegar de volta ao destino original.
Em termos de UX, isso evita fricção: o usuário clica em um link profundo (por exemplo, um item específico em “/orders/123?tab=invoice”), é solicitado a autenticar e, ao concluir, retorna exatamente para aquele item e contexto.

Em termos técnicos, o returnUrl pode ser transportado de duas formas comuns:
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
- Via state de navegação do React Router (recomendado): não aparece na URL, é mais difícil de ser manipulado externamente e reduz risco de open redirect.
- Via query string (ex.:
/login?returnUrl=/orders/123): útil quando você precisa preservar o destino mesmo após refresh na tela de login, mas exige validação cuidadosa para evitar redirecionamento aberto (open redirect).
Objetivo prático: fluxo completo
O fluxo que você quer alcançar é:
- Usuário tenta acessar
/area/relatorios?range=30dsem estar autenticado. - Guard/rota privada detecta ausência de sessão e redireciona para
/login, preservando o destino pretendido. - Usuário faz login.
- Aplicação navega para o destino preservado (
/area/relatorios?range=30d) e substitui o histórico para evitar voltar ao login ao apertar “voltar”.
Abordagem recomendada: usando location.state (React Router)
Quando você redireciona para o login, você pode anexar ao redirecionamento um objeto de estado contendo a rota original. No React Router, isso é feito com <Navigate to="/login" state={{ from: location }} replace />. Depois, na página de login, você lê location.state?.from e navega para lá após autenticar.
Passo 1: capturar a rota pretendida no guard
Você já tem um mecanismo de rota privada/guard em capítulos anteriores; aqui, o foco é apenas no detalhe de preservar o destino. A ideia é: ao negar acesso, capture o location atual e passe adiante.
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 null; if (!isAuthenticated) { return ( <Navigate to="/login" replace state={{ from: location }} /> ); } return <Outlet />; }Pontos importantes:
state={{ from: location }}carrega o objeto completo de localização, incluindopathname,searchehash. Isso preserva query string e hash automaticamente.replaceevita que a rota protegida “negada” fique no histórico. Assim, ao apertar “voltar”, o usuário não cai em um loop de redirecionamento.isLoadingevita “flicker” (mostrar conteúdo e redirecionar logo depois) enquanto você ainda está restaurando sessão.
Passo 2: ler o destino na tela de login
Na página de login, você precisa ler o state e decidir para onde navegar após autenticar. Se não houver destino (por exemplo, usuário abriu diretamente /login), você define um fallback.
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; const returnTo = from?.pathname ? `${from.pathname}${from.search || ""}${from.hash || ""}` : "/"; async function handleSubmit(e) { e.preventDefault(); const email = e.target.email.value; const password = e.target.password.value; const ok = await signIn({ email, password }); if (ok) { navigate(returnTo, { replace: true }); } } return ( <form onSubmit={handleSubmit}> <input name="email" /> <input name="password" type="password" /> <button type="submit">Entrar</button> </form> ); }Por que montar returnTo como string?
- O
navigateaceita tanto string quanto objeto de localização. Usar string facilita e garante quesearchehashsejam preservados. - Você evita depender do formato interno do objeto
location(embora seja estável, a string é simples).
Por que replace: true no pós-login?
- Evita que o usuário volte para
/loginao apertar “voltar”. Em geral, após autenticar, voltar para login não faz sentido e pode confundir.
Passo 3: lidar com “já autenticado” acessando /login
Um caso comum: usuário autenticado digita /login na barra ou clica em um link antigo. Você pode redirecionar automaticamente para um destino padrão, ou para um from se existir (por exemplo, se o login foi aberto por um guard e a sessão foi restaurada no meio do caminho).
import { useEffect } from "react"; import { useLocation, useNavigate } from "react-router-dom"; import { useAuth } from "../auth/useAuth"; export function LoginPage() { const { isAuthenticated } = useAuth(); const navigate = useNavigate(); const location = useLocation(); const from = location.state?.from; const returnTo = from?.pathname ? `${from.pathname}${from.search || ""}${from.hash || ""}` : "/"; useEffect(() => { if (isAuthenticated) { navigate(returnTo, { replace: true }); } }, [isAuthenticated, navigate, returnTo]); return ( <div>...form...</div> ); }Isso reduz “becos sem saída”: se a sessão já existe, o login não deve bloquear o usuário.
Preservando também o contexto: query string, hash e estado
O exemplo acima preserva pathname, search e hash. Isso cobre a maioria dos casos, como filtros, abas e âncoras.
Mas existe um detalhe: React Router também permite navegação com state (por exemplo, navigate("/checkout", { state: { coupon: "X" } })). Esse estado não aparece na URL e pode ser importante. Se você quiser preservar esse estado também, você pode guardar o objeto location inteiro e navegar usando um objeto.
const from = location.state?.from; // pode conter state interno também // ... após login: if (from) { navigate(from.pathname + (from.search || "") + (from.hash || ""), { replace: true, state: from.state, }); } else { navigate("/", { replace: true }); }Use isso com parcimônia: estado de navegação pode conter dados não serializáveis ou sensíveis. Em geral, o ideal é que o que precisa sobreviver a refresh esteja na URL (query string) ou em um estado global controlado.
Quando usar query string (?returnUrl=) e como fazer com segurança
Há cenários em que location.state não é suficiente:
- O usuário é redirecionado para login e dá refresh na página de login: o
statepode se perder dependendo do fluxo e do navegador. - Você precisa compartilhar o link de login já com destino (por exemplo, um e-mail interno que manda para login e depois para uma página específica).
Nesses casos, você pode usar query string, mas com validação rígida para evitar open redirect (um atacante poderia criar um link /login?returnUrl=https://site-malicioso.com e, após login, você redirecionaria para fora do seu domínio).

Passo a passo: construir o link de login com returnUrl
No guard, em vez de usar state, você pode montar a URL do login com o destino atual codificado.
import { Navigate, useLocation } from "react-router-dom"; import { useAuth } from "../auth/useAuth"; export function RequireAuthQuery() { const { isAuthenticated } = useAuth(); const location = useLocation(); if (!isAuthenticated) { const intended = `${location.pathname}${location.search || ""}${location.hash || ""}`; const returnUrl = encodeURIComponent(intended); return <Navigate to={`/login?returnUrl=${returnUrl}`} replace />; } return null; }Passo a passo: ler e validar o returnUrl no login
Você deve aceitar apenas destinos internos, preferencialmente começando com / e sem protocolo. Também é útil bloquear padrões como // (que alguns navegadores interpretam como URL relativa ao protocolo) e bloquear http:/https:.
import { useMemo } from "react"; import { useLocation, useNavigate } from "react-router-dom"; function isSafeInternalPath(path) { if (!path) return false; if (path.startsWith("http://") || path.startsWith("https://")) return false; if (!path.startsWith("/")) return false; if (path.startsWith("//")) return false; return true; } export function LoginPage() { const location = useLocation(); const navigate = useNavigate(); const params = useMemo(() => new URLSearchParams(location.search), [location.search]); const raw = params.get("returnUrl"); const decoded = raw ? decodeURIComponent(raw) : null; const returnTo = isSafeInternalPath(decoded) ? decoded : "/"; async function handleLoginSuccess() { navigate(returnTo, { replace: true }); } return ( <div>...</div> ); }Regras práticas de segurança:
- Nunca redirecione para uma URL absoluta fornecida pelo usuário.
- Permita apenas caminhos internos (começando com
/) e, se possível, valide contra uma lista de rotas conhecidas ou prefixos permitidos (ex.:/app,/area). - Evite aceitar
returnUrlcom caracteres estranhos que possam ser interpretados como esquema (ex.:javascript:). A checagem acima já bloqueia os casos mais comuns, mas você pode endurecer ainda mais.
Evitando loops e destinos inválidos
Um erro comum é salvar como returnUrl uma rota que inevitavelmente redireciona de novo para login, causando loop. Exemplos:
- Salvar
/logincomo returnUrl (o usuário faz login e volta para login). - Salvar uma rota de logout ou uma rota que exige um papel/permissão que o usuário não tem.
Boas práticas:
- Se o destino pretendido for
/login, ignore e use fallback. - Se o destino pretendido for uma rota que exige permissões, você pode deixar o guard tratar depois do login. Nesse caso, se o usuário não tiver permissão, ele será redirecionado para uma página de “acesso negado” (ou fallback). O importante é evitar loop: se “acesso negado” também redireciona para login, corrija isso.
function normalizeReturnTo(path) { if (!path) return "/"; if (path === "/login") return "/"; return path; }Preservação do returnUrl em fluxos com múltiplas etapas
Alguns logins têm etapas adicionais: MFA/2FA, troca de senha no primeiro acesso, aceite de termos, seleção de organização/tenant. Nesses casos, o returnUrl precisa sobreviver a várias telas.
Duas estratégias comuns:
- Manter em query string durante o fluxo:
/login?returnUrl=...→/mfa?returnUrl=...→/terms?returnUrl=.... Exige validação em cada etapa, mas é simples e resiliente a refresh. - Persistir temporariamente em memória/armazenamento: salvar o returnUrl em um estado global (ou sessionStorage) quando o guard redireciona, e consumi-lo ao final do fluxo. Isso evita poluir URLs, mas precisa de limpeza e cuidado para não ficar “stale”.
Exemplo com sessionStorage (útil se você não quer query string e precisa sobreviver a refresh):
// no guard: const intended = `${location.pathname}${location.search || ""}${location.hash || ""}`; sessionStorage.setItem("returnUrl", intended); return <Navigate to="/login" replace />; // no login (após sucesso): const stored = sessionStorage.getItem("returnUrl"); sessionStorage.removeItem("returnUrl"); const returnTo = isSafeInternalPath(stored) ? stored : "/"; navigate(returnTo, { replace: true });Cuidados:
- Limpe o valor após usar para evitar que um login futuro redirecione para um destino antigo.
- Valide o caminho mesmo vindo do storage (não confie cegamente).
Interação com expiração de sessão e redirecionamento automático
Em SPAs, a sessão pode expirar enquanto o usuário está em uma rota protegida. Quando isso acontece, você pode:
- Redirecionar para login e preservar a rota atual como returnUrl.
- Após reautenticar, retornar para a mesma tela e permitir que o usuário continue.
O detalhe é evitar que o redirecionamento aconteça no meio de uma ação crítica (por exemplo, envio de formulário). Uma abordagem prática é: ao detectar 401 em uma chamada, você pode disparar um fluxo de reautenticação e, ao final, repetir a ação ou ao menos retornar o usuário para a tela com o estado preservado na URL. Mesmo sem repetir automaticamente, preservar o returnUrl já reduz perda de contexto.
Exemplo integrado: guard + login com state e fallback seguro
A seguir, um exemplo mais completo, combinando as ideias essenciais: guardar a rota via state, montar o retorno com pathname/search/hash, evitar voltar para login e aplicar fallback.
// RequireAuth.jsx 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 null; if (!isAuthenticated) { return ( <Navigate to="/login" replace state={{ from: location }} /> ); } return <Outlet />; } // LoginPage.jsx import { useMemo } from "react"; import { useLocation, useNavigate } from "react-router-dom"; import { useAuth } from "../auth/useAuth"; function buildReturnTo(fromLocation) { if (!fromLocation?.pathname) return "/"; const path = `${fromLocation.pathname}${fromLocation.search || ""}${fromLocation.hash || ""}`; if (path === "/login") return "/"; return path; } export function LoginPage() { const { signIn } = useAuth(); const navigate = useNavigate(); const location = useLocation(); const returnTo = useMemo(() => buildReturnTo(location.state?.from), [location.state]); async function onSubmit(e) { e.preventDefault(); const email = e.target.email.value; const password = e.target.password.value; const ok = await signIn({ email, password }); if (ok) navigate(returnTo, { replace: true }); } return ( <form onSubmit={onSubmit}> <input name="email" /> <input name="password" type="password" /> <button type="submit">Entrar</button> </form> ); }Checklist de implementação (para evitar bugs comuns)
- O guard deve usar
replaceao enviar para login, para evitar histórico “poluído” e loops no botão voltar. - O pós-login deve usar
replaceao navegar para o returnUrl, para evitar voltar ao login. - Preserve
searchehashpara manter filtros/abas/âncoras. - Defina fallback quando não houver returnUrl (ex.:
/ou uma rota inicial da área autenticada). - Evite returnUrl apontando para
/logine normalize destinos. - Se usar query string, valide para aceitar apenas caminhos internos e bloquear URLs absolutas (mitigação de open redirect).
- Se o fluxo de login tiver múltiplas etapas, escolha uma estratégia para persistir o returnUrl (query string ou sessionStorage) e limpe ao final.