A pessoa busca um termo, escolhe uma categoria, abre um produto e volta à lista. O campo de busca mostra o termo anterior, a URL aponta para outra categoria e os resultados pertencem à primeira página de uma consulta que já mudou. Cada parte funciona isoladamente; a tela inteira conta três histórias.
O desencontro começa quando o mesmo filtro mora no componente, em uma store global e na URL, enquanto os resultados são copiados para outro estado local. Uma alteração precisa atravessar todos esses lugares, e qualquer caminho esquecido deixa uma versão para trás. Acrescentar outra sincronização pode corrigir um clique e criar um conflito na próxima navegação.
Antes de escolher a biblioteca de estado, vale definir quem pode mudar cada informação, quanto tempo ela precisa durar e o que deve acontecer ao recarregar ou compartilhar a página. Essas respostas organizam a interface melhor do que decidir que “tudo vai para a store” ou “tudo vai para a URL”.
Compartilhar uma tela não é compartilhar toda a memória dela
Em um catálogo, um link útil pode representar busca, categoria e ordenação. Ao abrir esse endereço em outra aba, a pessoa espera reencontrar a mesma seleção de critérios. Ela não necessariamente espera recuperar o menu que estava aberto, o texto que ainda não submeteu ou o estado de hover de uma linha.
A URL é uma boa candidata para escolhas que identificam uma visão navegável. O componente pode manter detalhes temporários de interação. O servidor determina quais produtos existem, seus preços e quais registros o usuário pode consultar. A camada de dados do frontend mantém uma cópia desses resultados com regras de atualização.
Essa separação de responsabilidades pode orientar tanto o armazenamento quanto o comportamento da tela:
| Informação | Referência principal | Comportamento esperado |
|---|---|---|
| Busca e filtros confirmados | URL | Sobrevivem ao recarregamento e podem ser compartilhados |
| Texto ainda não submetido | Formulário local | Pode mudar sem alterar os resultados |
| Menu de uma linha aberto | Componente | Desaparece ao sair daquele contexto |
| Produtos, preços e permissões | Servidor | São obtidos e validados para o usuário atual |
| Resultados já consultados | Cache de dados do cliente | Podem ser reutilizados conforme identidade e frescor |
| Quantidade de filtros ativos | Valor calculado | Deriva dos filtros, sem outra cópia editável |
Essa divisão é uma escolha do produto, não uma regra universal. Um editor pode preservar rascunhos entre visitas; uma aba de detalhes pode merecer uma URL própria. O critério é o comportamento que a informação precisa ter, e não seu tamanho ou o nome do hook que a armazena.
Quando URL e componente tentam mandar ao mesmo tempo
Um desenho frágil lê o parâmetro da URL no primeiro render, copia para um estado local e passa a atualizar os dois por effects independentes. O clique em “Voltar” altera a URL, um effect atualiza o componente e outro escreve de novo o valor que ainda estava em memória. A tentativa de sincronização pode sobrescrever uma navegação legítima.
Para os filtros confirmados do catálogo, podemos escolher a URL como referência. Os controles exibem valores derivados dela e as ações de confirmação produzem uma nova navegação. Assim, abrir um link, recarregar, usar Voltar e selecionar uma categoria entram pelo mesmo caminho de leitura.
O rascunho do campo de busca continua sendo outro estado, com outro significado: aquilo que a pessoa está digitando antes de confirmar. Ele não precisa ser uma cópia permanentemente sincronizada da busca ativa. Ao submeter, o produto transforma o rascunho em uma escolha confirmada. Ao navegar pelo histórico, pode descartar o rascunho ou pedir confirmação se houver trabalho relevante; essa política precisa ser explícita.
As orientações do React sobre estrutura de estado ajudam a evitar informações duplicadas e valores que podem ser calculados. Já a documentação do React Router sobre gerenciamento de estado mostra como parâmetros e dados carregados pelas rotas podem reduzir cópias manuais. São exemplos de ferramentas apoiando uma decisão que continua pertencendo à aplicação.
A confirmação deve mudar um conjunto coerente de critérios
Quando a pessoa troca a categoria, um cursor da consulta anterior pode deixar de fazer sentido. Atualizar apenas categoria e manter after permite pedir uma continuação de outra lista. O mesmo cuidado vale para a ordenação e para qualquer filtro que altere o conjunto consultado.
Podemos representar essa transição em TypeScript, sem depender de um router específico:
type CatalogFilters = {
q: string;
category: string | null;
order: 'name' | 'price';
};
type CatalogSearch = CatalogFilters & {
after: string | null;
};
function applyFilters(next: CatalogFilters): CatalogSearch {
return { ...next, after: null };
}
O argumento contém o conjunto completo de filtros já validado. A intenção é confirmar os critérios e reiniciar a continuação na mesma atualização. A serialização desse resultado deve produzir uma única navegação, em vez de disparar uma alteração para cada parâmetro. Isso reduz estados intermediários e consultas desnecessárias.
O artigo sobre paginação por offset e cursor aprofunda a fronteira usada pelo backend. Aqui, o ponto é preservar a coerência da interface quando essa fronteira depende dos filtros atuais.
A leitura da URL também precisa de um contrato: valores permitidos para ordenação, limites para o termo, formato da categoria e comportamento diante de parâmetros desconhecidos. Remover valores padrão pode deixar o link mais limpo, desde que a interpretação permaneça consistente. Se a intenção dos defaults mudar no futuro, links antigos podem passar a mostrar outra visão; filtros salvos que exigem estabilidade precisam considerar essa evolução.
As APIs nativas apresentadas em URLSearchParams cuidam da representação dos parâmetros. Elas não escolhem esses defaults nem validam regras de negócio. O servidor continua validando o pedido e a autorização sobre os dados, mesmo quando o link foi gerado pela própria interface.
Voltar precisa representar decisões reconhecíveis
Nem toda atualização deve criar uma nova entrada no histórico. Se cada letra digitada fizer um push, voltar da busca “teclado” pode exigir atravessar vários prefixos do mesmo termo. Por outro lado, substituir todas as entradas pode impedir que a pessoa retorne à categoria anterior.
Confirmar uma busca, aplicar uma categoria ou abrir outra visão do catálogo são ações que podem justificar uma nova entrada. Correções de representação e alterações contínuas de um mesmo controle podem substituir a entrada atual. O comportamento deve acompanhar a experiência pretendida, inclusive se a busca for automática em vez de confirmada por um botão.
O pushState nativo cria uma entrada; replaceState substitui a atual. Nenhum dos dois dispara popstate por conta própria. Esse evento participa da travessia do histórico. A MDN documenta essa distinção, que importa para aplicações que implementam sua própria reação à navegação.
Quando existe um router, é mais previsível usar sua API para escrever os filtros. Misturar alterações diretas no histórico com a navegação do framework pode deixar a URL visível e o estado conhecido pelo router fora de acordo. Um relato no nuqs aberto em agosto de 2026 descreve justamente um atrito entre uma atualização por push e o histórico mantido pelo React Router em uma configuração específica. Ele ilustra o tipo de integração que precisa ser conferido, sem significar que todos os routers tenham esse comportamento.
Bibliotecas de parâmetros podem ajudar com parsing, atualização e integração. A documentação do nuqs distingue, por exemplo, alterações no histórico e notificações ao servidor. A ferramenta não decide se mudar uma ordenação deve ser uma etapa navegável no seu produto. Essa decisão vem antes da configuração.
Os resultados têm uma identidade diferente da URL inteira
A URL identifica a visão, mas nem todo parâmetro muda o resultado. Uma preferência puramente visual pode alterar a apresentação sem exigir outra consulta. Já um filtro omitido da chave do cache pode fazer a aplicação reutilizar uma resposta de outra busca.
Uma chave para os resultados do catálogo pode incluir o contexto autenticado da organização e todos os critérios que alteram a consulta:
const resultKey = [
'catalog',
workspaceId,
{
q: search.q,
category: search.category,
order: search.order,
after: search.after,
},
];
Essa estrutura segue a ideia de chaves documentada pelo TanStack Query: variáveis que mudam o conjunto retornado participam da identidade da consulta. O exemplo não exige adotar essa biblioteca. Um cache próprio também precisa distinguir essas consultas de alguma forma.
workspaceId vem do contexto da aplicação para organizar o cache; o backend confirma que o usuário tem acesso à organização. Se a resposta depende também do usuário ou de outro contexto de autorização, a identidade do cache deve refletir isso. Na troca de conta, caches e requisições em andamento precisam ser isolados ou descartados conforme o desenho, evitando mostrar dados do contexto anterior.
Uma resposta atrasada da categoria anterior pode continuar válida para a chave anterior. Ela não deve substituir a visão atual. A camada de dados precisa associar cada resultado aos critérios que o produziram, mantendo separado o estado de carregamento da busca ativa. Cancelar chamadas pode economizar trabalho, mas a correção também depende dessa identidade.
O frescor é outra decisão. Reutilizar uma lista por alguns segundos pode ser aceitável; confirmar um preço ou uma operação usa a validação atual do servidor. E o cache em memória da aplicação tem regras diferentes do cache HTTP. As diretivas explicadas em Cache-Control continuam necessárias quando respostas passam por navegadores, proxies ou CDNs.
Persistência e compartilhamento têm limites
Colocar uma informação na URL facilita copiar a visão, mas também a expõe a locais que registram o endereço, como histórico, logs e ferramentas de analytics. Segredos e credenciais não devem entrar nesse contrato. Termos de busca com dados pessoais também exigem cuidado; compartilhável não significa adequado para qualquer conteúdo.
Guardar um formulário em armazenamento local é uma escolha diferente. É preciso decidir a quem o rascunho pertence, quando vence e o que acontece ao sair da conta ou trocar de organização. Um rascunho persistido não pode reaparecer como trabalho de outra pessoa apenas porque o navegador continua sendo o mesmo.
Na renderização inicial, cliente e servidor devem interpretar os filtros com as mesmas regras. Se o servidor assume uma ordenação e o cliente escolhe outra ao montar, a tela pode mudar de resultado e repetir a consulta logo depois de abrir. Um contrato compartilhado de leitura e defaults ajuda a manter esse primeiro estado coerente, qualquer que seja a estratégia de renderização.
Escolha uma store para o estado que precisa dela
Depois dessa divisão, uma store compartilhada pode continuar fazendo sentido para um fluxo entre telas, uma seleção que atravessa componentes ou um rascunho complexo. O motivo é o alcance e o ciclo de vida desse estado. Copiar para a store uma lista que já tem cache e regras de revalidação cria outro lugar a manter atualizado sem necessariamente acrescentar comportamento útil.
Uma forma prática de revisar a tela é seguir uma informação por vez: onde ela nasce, quem pode alterá-la, como é reconstruída após recarregar e quando deixa de valer. No catálogo, a busca confirmada segue a URL; o texto ainda não enviado segue o formulário; os resultados seguem a identidade da consulta; as permissões seguem o servidor.
Com essas responsabilidades definidas, os casos de abrir um link direto, voltar de um produto e trocar de conta deixam de ser exceções remendadas no fim. Eles passam a ser consequências do mesmo modelo de estado que organiza o restante da interface.

