Web Concurrency (concorrência no navegador)

· Padrão Frontend
Juan Kalleo
Juan Kalleo
Senior Fullstack
This page hasn't been translated to English yet — showing the original Portuguese content.

JavaScript no navegador roda numa única thread — não existe race condition de memória compartilhada como em linguagem multi-thread. Mas isso não elimina concorrência: chamadas assíncronas (fetch, setTimeout) ainda podem resolver fora de ordem. Um exemplo real: o usuário digita um filtro, dispara uma busca; digita de novo antes da primeira resposta voltar, dispara uma segunda; se a primeira responder depois da segunda, a tela acaba mostrando o resultado desatualizado — mesmo que nada tenha corrompido, a UI ficou inconsistente com a intenção mais recente do usuário.

No padrão frontend

Duas estratégias diferentes convivem no projeto, cada uma no lugar certo:

Toda tela que busca dado via useQuery/useAdminGet/useAdminList (a maioria do projeto, ver Dados e API) já não sofre esse problema — o TanStack Query rastreia cada chamada por queryKey; se o filtro muda e dispara uma nova busca antes da anterior responder, o resultado da busca antiga é descartado internamente, mesmo que a requisição HTTP em si ainda esteja em voo. Nenhum código deste projeto precisa de guarda manual pra isso — é comportamento de biblioteca, de graça.

Nuance real, não escondida: isso evita a UI mostrar dado velho, mas não cancela a requisição HTTP em si — o navegador ainda espera a resposta antiga chegar, só ignora o resultado. Cancelamento de verdade exigiria passar o signal que o TanStack Query already oferece pro queryFn adiante, até o fetch() real (services/api-admin.ts#request(), que já aceita RequestInit — inclusive signal — mas nenhum hook do projeto passa isso hoje). Não é bug: pra requisição GET idempotente e rápida contra a API interna deste projeto, o custo de deixar a resposta antiga terminar (e ser descartada) é desprezível — cancelamento explícito só valeria a pena pra requisição cara/lenta.

As duas exceções que usam guarda manual — os hooks de preview de relatório (use-relatorio-usuarios-pdf-preview.ts, use-relatorio-usuarios-excel-preview.ts) — ficam fora do TanStack Query de propósito (são geração de arquivo com efeito colateral, não uma query cacheável), então precisam da própria guarda contra resposta fora de ordem:

useEffect(() => {
  let cancelado = false;

  (async () => {
    const resultado = await adminList(/* ... */);
    const novoBlob = await gerarRelatorioUsuariosPdf(/* ... */);
    if (!cancelado) setBlob(novoBlob);
  })();

  return () => {
    cancelado = true;
  };
}, [/* deps */]);

O flag cancelado, fechado sobre esse efeito específico, garante que se o componente desmontar (ou os parâmetros mudarem, disparando o efeito de novo) antes da geração terminar, o resultado antigo nunca chega a setState — é o mesmo problema do TanStack Query, resolvido à mão porque esses dois hooks estão fora do alcance dele.

Leitura de apoio