A maior parte do código frontend trata o trabalho assíncrono como uma única forma. Você inicia uma Promise, espera que ela seja resolvida, despeja o resultado no estado local e deixa o framework reconciliar a diferença. Esse padrão é sedutor porque funciona em todos os lugares: uma chamada REST, o envio de um formulário, uma mensagem WebSocket. Todos terminam no mesmo useEffect ou manipulador de eventos, todos passam pelo setState e todos parecem uma infraestrutura assíncrona idêntica. Essa uniformidade é uma armadilha. Em uma aplicação real, nem todo trabalho assíncrono é igual. Fingir que é transforma seus componentes de UI em arquitetos de dados acidentais, remendados com hooks useEffect e dedos cruzados.

As operações assíncronas, na verdade, dividem-se em três espécies diferentes. Cada uma tem uma relação distinta com tempo, cache e propriedade. Aprender a diferenciá-las é o que mantém um frontend rápido, correto e saudável.

Queries: Fatos com um Endereço

Uma query não é apenas um fetch. É uma requisição por um fato identificável. Você está pedindo por /user/123, não por "alguns dados de usuário". Essa distinção é importante porque a identidade é o que torna o cache possível. Se dois componentes na mesma tela precisam do mesmo registro de usuário, eles devem compartilhar uma única resposta. Quando cada componente mantém sua própria cópia local no useState, você fragmenta a sua verdade. O avatar no cabeçalho e o nome na barra lateral ficam desalinhados porque foram buscados em momentos diferentes, ou porque um falhou enquanto o outro teve sucesso.

Pense em uma query como um recurso, não como uma ação. Ela possui uma chave de cache, uma política de validade e um ciclo de vida que sobrevive a qualquer componente individual. Uma camada de query bem construída entende que ler /projects?page=2 é diferente de ler /projects?page=3. Cada URL e conjunto de parâmetros forma um endereço, e os dados nesse endereço podem estar obsoletos, atualizados ou ausentes. A UI não deve ser responsável por essa gestão. Ela deve solicitar user:123 à camada de dados e receber um snapshot. Se esse snapshot veio do servidor há dois segundos ou de um cache há dois milissegundos não é da conta do componente.

As consequências práticas são imediatas. Quando você trata cada leitura como um fetch imperativo dentro de um componente, você perde a deduplicação. Você perde a atualização em segundo plano. Você perde a capacidade de mostrar dados em cache instantaneamente enquanto os valida em segundo plano. Uma query merece um lugar fora da sua árvore de UI.

Mutations: Mudando o Mundo

Se as queries perguntam sobre o mundo, as mutations o mudam. A requisição de rede real — o POST, PUT ou DELETE — geralmente é a parte fácil. A parte difícil é tudo o que acontece depois que o servidor diz "OK".

Suponha que um usuário atualize seu nome de exibição. A mutation em si é uma única requisição. Mas o raio de impacto está em toda parte. A página de perfil mantém o nome antigo. A barra de navegação mostra o nome antigo. O histórico de comentários pode fazer referência a ele. Se o seu código de mutation não faz nada além de alternar uma flag isLoading local e depois atualizar uma parte do estado, sua aplicação está mentindo para si mesma. Alguns cantos da UI fingem que a mudança ocorreu. Outros não têm ideia de que algo mudou.

Uma mutation deve declarar seu impacto no grafo de dados. Ela deve informar ao sistema quais queries agora são inválidas, quais chaves de cache precisam ser buscadas novamente e quais relacionamentos mudaram. Isso é fundamentalmente diferente de uma query. Uma query é apenas de leitura e compartilhável. Uma mutation é focada em escrita e destrutiva para os caches existentes. Achatar ambas na mesma abstração significa que os desenvolvedores acabam chamando refetch() manualmente dentro de componentes aleatórios ou, pior, espalhando hooks useEffect pela árvore para "sincronizar" o estado local de volta ao alinhamento com o servidor.

O modelo de propriedade também é diferente. Queries geralmente pertencem ao cache. Uma mutation pertence à