Como funciona o gerenciamento de histórico em aplicações web na prática
Se você já tentou criar um SPA que não quebra o botão voltar do navegador quando alguém clica nele, sabe que o histórico não é algo que se resolve com um setTimeout e uma oração. A HTML5 History API existe desde 2010 e ainda vejo gente implementando rotas manualmente com localStorage toda semana. Vou explicar como fazer direito, sem enrolação.
O que é coisa de história e por que ela importa
"Coisa de história" no contexto de desenvolvimento web refere-se ao manejo do histórico do navegador usando pushState, replaceState e o evento popstate. O problema real começa quando você tem uma aplicação React ou Vue que redireciona internamente mas o usuário espera que o botão voltar funcione como num site tradicional. Os navegadores não entendem que "/" e "/dashboard" são páginas diferentes quando tudo é carregado via JavaScript. Semção manual, cada navegação pode destruir o estado da aplicação inteira. Comece entendendo os três métodos disponíveis. pushState empurra uma nova entrada no histórico sem recarregar a página. replaceState substitui a entrada atual — útil quando você quer corrigir uma URL antes que o usuário volte. popstate é o gatilho que dispara quando o usuário clica em voltar ou avançar. A parte que ninguém ensina nos tutoriais básicos é que popstate só dispara para entradas criadas via pushState, não para navegação pura de link.
Implementação prática passo a passo
A estrutura básica que você vai usar em qualquer projeto séria segue este padrão. Primeiro, registre o listener de popstate antes de qualquer navegação. Segundo, mantenha o estado da aplicação sincronizado com a entrada atual do histórico. Terceiro, evite o erro mais comum: chamar pushState em resposta a popstate, o que cria loops infinitos que travam a aba em segundos.
history.pushState({page: 'detalhe', id: 42}, '', '/produto/42');
window.addEventListener('popstate', (event) => {
if (event.state) {
renderizarPorEstado(event.state);
}
});
Isso resolve 80% dos casos. O problema é que na prática você raramente lida com apenas uma rota. Quando sua aplicação tem múltiplos views sobrepostos, modais, abas internas, o histórico precisa refletir isso ou o usuário vai acabar preso em um estado que não corresponde à URL mostrada na barra de endereço.
👉 Clique no botão abaixo para saber mais sobre o assunto!
O problema que ninguém menciona: dados vs estado
Eu perdi cerca de três dias num projeto de e-commerce em 2022 porque não distinguia dados carregados do estado da UI. O usuário navegava para um produto, eu chamava pushState com o ID. Quando clicava voltar, o popstate disparava, eu recuperava o estado pelo ID, mas os dados da requisição anterior ainda estavam na memória. A interface mostrava informações de um produto diferente por alguns milissegundos antes do carregamento novo replacing. Parecia um bug de renderização, mas era simplesmente o histórico e o cache de dados desencontrados. A solução foi trivial mas difícil de diagnosticar: separar completamente o estado de navegação do estado de dados. O pushState passa apenas metadados de roteamento, nunca objetos grandes com payloads. Os dados vêm de uma camada separada, preferencialmente com invalidation baseada no ID, não no tempo. Se você está guardando conteúdo da API dentro do state passado ao pushState, está fazendo errado.
Edge case que quebra muita gente: URLs compartilháveis
Um usuário copia a URL de um produto e manda para outro. O destinatário abre, a aplicação carrega, mas o histórico agora tem duas entradas: a raiz e o produto. Se ele clicar voltar, o popstate dispara, mas o estado inicial da aplicação pode não ter sido restaurado corretamente porque o listener de popstate só foi registrado após o primeiro pushState. O fix é registrar o popstate listener antes do primeiro pushState, sim, isso é óbvio mas eu vejo frameworks inteiros fazendo na ordem errada. Além disso, considere o caso em que o servidor não responde com o mesmo HTML para rotas dinâmicas. Se um link direto para /produto/42 chega ao servidor e ele retorna uma página estática em vez do shell da aplicação, o usuário vê conteúdo correto mas o JavaScript nunca monta o estado correto. Isso exige server-side configuration ou fallback no service worker. Nada disso é complexo, só exige que você pense nisso antes de produzir.
Alternativas para quem não quer gerenciar histórico manualmente
Se a coisa de história está te dando dor de cabeça desnecessária, use uma biblioteca de roteamento madura. React Router, Vue Router e Solid Router tratam do histórico automaticamente, incluem suporte a history API, fallback para hash routing quando necessário, e lidam com edge cases que você nunca iria considerar. A desvantagem é que você perde controle fino sobre o que entra no histórico, o que pode ser problema se seu app precisa de comportamento customizado como guardar estado de scroll entre navegações ou implementar undo de navegação. Para apps pequenos, a API nativa é suficiente e evita dependência extra. Para apps médios e grandes, a sobrecarga de uma biblioteca de roteamento compensa porque a maior parte dos bugs de histórico que aparecem em produção vem de implementação manual incompleta. Não tente recriar o que o React Router já faz há uma década, especialmente se seu prazo é apertado.
Pitfalls comuns e como evitá-los
O primeiro erro é chamar pushState sem atualizar o conteúdo da página. Você cria uma entrada no histórico mas o usuário vê a mesma coisa. A correção é garantir que o estado visível e a entrada do histórico sejam atômicos, atualizados na mesma operação síncrona. O segundo erro é confiar que event.state sempre será definido no popstate. Navegação direta por URL, reload e botões do navegador podem disparar popstate com state igual a null. Sempre faça fallback para a URL atual usando window.location quando state for null. O terceiro erro é não pensar em performance. Cada chamada de pushState é barata mas se você está em uma lista com scroll infinito e atualiza o histórico a cada item visível, vai notar lag. Limite a frequência de atualizações de histórico para eventos explícitos do usuário, nunca para mudanças automáticas de estado da UI. Em um projeto recente eu reduzi de 60 pushStates por segundo para 2 limitando as atualizações a interações intencionais do usuário e o scroll ficou fluido de novo.
O quarto erro, e o mais silencioso, é esquecer que replaceState não adiciona entrada nova. Se você usa replaceState para corrigir a URL após uma redirecionamento mas depois precisa que o usuário possa voltar para a URL original, ela não existe mais no histórico. Documente claramente onde você usa cada um dos dois métodos para não se perder semanas depois debugando comportamento inesperado do botão voltar. Se quiser ver a documentação oficial, está em developer.mozilla.org/en-US/docs/Web/API/History_api. A maioria dos problemas que encontrei na prática não estão na API em si, mas na forma como a aplicação lida com ela. O histórico é simples até você precisar que ele funcione em todas as condições possíveis.