<h2>Introdução ao React Query e o Ciclo de Vida das Mutações</h2>
<p>React Query é uma biblioteca que gerencia estado de servidor em aplicações React, permitindo que você trabalhe com dados remotos de forma previsível e eficiente. Diferentemente do Redux ou Context API, que gerenciam principalmente estado local, React Query se especializa em sincronizar estado do servidor com a interface do usuário. Mutações são operações que alteram dados no servidor — criações, atualizações e exclusões — e representam um ponto crítico onde a sincronização entre cliente e servidor pode quebrar se não for bem implementada.</p>
<p>O grande diferencial do React Query em relação a outras abordagens é seu sistema robusto de invalidação de cache e sincronização automática. Quando você executa uma mutação, não basta apenas enviar os dados ao servidor; é necessário garantir que o cache local reflita essa mudança e que queries relacionadas sejam revalidadas. Sem isso, o usuário verá dados desatualizados, criando uma experiência confusa. Neste artigo, você aprenderá não apenas como criar mutações, mas como construir um sistema de cache inteligente que mantém sua aplicação sempre sincronizada.</p>
<h2>Fundamentos de Mutações com useMutation</h2>
<h3>Criando Sua Primeira Mutação</h3>
<p>Uma mutação no React Query é criada através do hook <code>useMutation</code>, que retorna um objeto com funções para disparar a mutação e estado associado. Diferentemente de <code>useQuery</code>, que é executado automaticamente, mutações apenas rodam quando explicitamente disparadas. Isso faz sentido: você não quer atualizar dados toda vez que um componente monta; você quer controlar exatamente quando isso acontece.</p>
<pre><code class="language-javascript">import { useMutation } from '@tanstack/react-query';
const useCreatePost = () => {
return useMutation({
mutationFn: async (newPost) => {
const response = await fetch('/api/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newPost),
});
if (!response.ok) {
throw new Error('Falha ao criar post');
}
return response.json();
},
});
};
export default useCreatePost;</code></pre>
<p>Neste exemplo, <code>mutationFn</code> é uma função assíncrona que envia dados ao servidor. O hook retorna um objeto contendo <code>mutate</code> (função para disparar), <code>isPending</code>, <code>isError</code>, <code>error</code> e <code>data</code>. Você chama <code>mutate</code> passando os argumentos que serão enviados para <code>mutationFn</code>.</p>
<h3>Estados e Ciclo de Vida</h3>
<p>Toda mutação passa por estados bem definidos: <code>idle</code> (não iniciada), <code>pending</code> (em progresso), <code>success</code> (completada com sucesso) ou <code>error</code> (falhou). Esses estados permitem que você renderize a interface apropriadamente — mostrando spinners durante o carregamento, mensagens de erro quando aplicável, ou feedback de sucesso.</p>
<pre><code class="language-javascript">import { useState } from 'react';
import useCreatePost from './useCreatePost';
export default function CreatePostForm() {
const [title, setTitle] = useState('');
const { mutate, isPending, isError, error, data } = useCreatePost();
const handleSubmit = (e) => {
e.preventDefault();
mutate({ title, content: 'Conteúdo do post' });
};
return (
<form onSubmit={handleSubmit}>
<input
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="Título do post"
/>
<button type="submit" disabled={isPending}>
{isPending ? 'Criando...' : 'Criar Post'}
</button>
{isError && <p style={{ color: 'red' }}>Erro: {error.message}</p>}
{data && <p style={{ color: 'green' }}>Post criado com ID: {data.id}</p>}
</form>
);
}</code></pre>
<p>Neste componente, o botão é desabilitado durante a mutação, o usuário recebe feedback visual de sucesso ou erro, e a interface responde em tempo real aos mudanças de estado. Esse padrão é fundamental para criar aplicações que se sentem responsivas.</p>
<h2>Invalidação de Cache: Sincronizando Realidade</h2>
<h3>O Problema da Inconsistência</h3>
<p>Imagine que você tem uma lista de posts em cache. Um usuário cria um novo post. A mutação retorna com sucesso, mas sua lista em cache ainda não inclui esse novo post. O usuário vê a mensagem "Post criado!", mas quando abre a lista, o post não está lá. Essa é uma inconsistência. Para resolver isso, você precisa invalidar o cache — dizer ao React Query que certos dados podem estar desatualizados e devem ser revalidados.</p>
<p>Invalidação é o processo de marcar queries específicas como "stale" (antigas), forçando o React Query a refetchá-las na próxima oportunidade. Você faz isso através do <code>useQueryClient</code> e seu método <code>invalidateQueries</code>.</p>
<pre><code class="language-javascript">import { useMutation, useQueryClient } from '@tanstack/react-query';
const useCreatePost = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (newPost) => {
const response = await fetch('/api/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newPost),
});
if (!response.ok) {
throw new Error('Falha ao criar post');
}
return response.json();
},
onSuccess: () => {
// Invalida a query que lista todos os posts
queryClient.invalidateQueries({ queryKey: ['posts'] });
},
});
};
export default useCreatePost;</code></pre>
<p>Aqui, após o sucesso da mutação, invalidamos qualquer query com <code>queryKey</code> que comece com <code>['posts']</code>. Isso força o React Query a refetchar a lista de posts, garantindo que o novo post apareça. O callback <code>onSuccess</code> é executado quando a mutação termina com sucesso.</p>
<h3>Invalidação Granular e Padrões</h3>
<p>React Query permite invalidações muito granulares. Você pode invalidar queries específicas, usar padrões com <code>exact: false</code> para invalidar toda uma família de queries, ou ser ainda mais específico.</p>
<pre><code class="language-javascript">const queryClient = useQueryClient();
// Invalida exatamente ['posts', 123]
queryClient.invalidateQueries({ queryKey: ['posts', 123] });
// Invalida todas as queries que começam com ['posts']
queryClient.invalidateQueries({ queryKey: ['posts'], exact: false });
// Invalida apenas se a query está em cache (não refetch automaticamente)
queryClient.invalidateQueries({
queryKey: ['posts'],
refetchType: 'none'
});</code></pre>
<p>Essa granularidade é poderosa. Se você atualiza um post específico, não precisa refetchar toda a lista — invalide apenas aquele post. Se a lista já está em cache, ela será revalidada apenas quando o usuário a visualizar novamente.</p>
<h2>Otimismo: Atualizando Cache Antes do Servidor Responder</h2>
<h3>Entendendo Atualizações Otimistas</h3>
<p>Atualizações otimistas são um padrão onde você atualiza o cache localmente antes do servidor confirmar a mudança. Se o servidor responder com sucesso, perfeito — o cache já estava correto. Se falhar, você reverte o cache para o estado anterior. Isso cria uma experiência muito mais rápida para o usuário, especialmente em conexões lentas.</p>
<pre><code class="language-javascript">import { useMutation, useQueryClient } from '@tanstack/react-query';
const useUpdatePost = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (updatedPost) => {
const response = await fetch(/api/posts/${updatedPost.id}, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(updatedPost),
});
if (!response.ok) {
throw new Error('Falha ao atualizar post');
}
return response.json();
},
onMutate: async (updatedPost) => {
// Cancela queries pendentes relacionadas
await queryClient.cancelQueries({ queryKey: ['posts', updatedPost.id] });
// Salva dados anteriores para rollback
const previousPost = queryClient.getQueryData(['posts', updatedPost.id]);
// Atualiza cache otimisticamente
queryClient.setQueryData(['posts', updatedPost.id], updatedPost);
return { previousPost };
},
onError: (err, updatedPost, context) => {
// Reverte ao estado anterior se a mutação falhar
if (context?.previousPost) {
queryClient.setQueryData(['posts', updatedPost.id], context.previousPost);
}
},
onSuccess: (data, updatedPost) => {
// Atualiza com dados reais do servidor
queryClient.setQueryData(['posts', updatedPost.id], data);
},
});
};
export default useUpdatePost;</code></pre>
<p>O fluxo aqui é: (1) <code>onMutate</code> é disparado antes da requisição, salvando o estado anterior e atualizando o cache localmente; (2) a requisição é enviada; (3) se falhar, <code>onError</code> reverte para o estado anterior; (4) se suceder, <code>onSuccess</code> confirma ou ajusta o cache com dados reais do servidor. Esse padrão elimina a sensação de atraso.</p>
<h3>Atualizando Listas Relacionadas</h3>
<p>Frequentemente, você precisa atualizar não apenas um item específico, mas também as listas que o contêm. Isso fica complexo rapidamente.</p>
<pre><code class="language-javascript">const useUpdatePost = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (updatedPost) => {
const response = await fetch(/api/posts/${updatedPost.id}, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(updatedPost),
});
if (!response.ok) throw new Error('Falha ao atualizar post');
return response.json();
},
onMutate: async (updatedPost) => {
await queryClient.cancelQueries({ queryKey: ['posts'] });
// Salva dados anteriores de todas as queries relacionadas
const previousPosts = queryClient.getQueriesData({ queryKey: ['posts'] });
// Atualiza o post em todas as queries
queryClient.setQueriesData(
{ queryKey: ['posts'] },
(oldData) => {
if (!oldData) return oldData;
// Se é um array (lista), encontra e atualiza o item
if (Array.isArray(oldData)) {
return oldData.map((post) =>
post.id === updatedPost.id ? updatedPost : post
);
}
return oldData;
}
);
return { previousPosts };
},
onError: (err, updatedPost, context) => {
// Reverte todas as queries
if (context?.previousPosts) {
context.previousPosts.forEach(([queryKey, data]) => {
queryClient.setQueryData(queryKey, data);
});
}
},
});
};</code></pre>
<p>Aqui, usamos <code>setQueriesData</code> para atualizar múltiplas queries de uma vez, mantendo a UI sincronizada em toda a aplicação. Se você tem a lista de posts, posts filtrados, e um post específico em cache, todos serão atualizados otimisticamente.</p>
<h2>Estratégias Avançadas: Sincronização em Tempo Real</h2>
<h3>Refetch Automático Baseado em Tipo de Mutação</h3>
<p>Em aplicações com múltiplas mutações, é comum precisar de diferentes estratégias de sincronização. Uma mutação que cria um post poderia refetchar a lista inteira, mas uma que atualiza apenas um post poderia usar atualização otimista. React Query permite encadear comportamentos através de callbacks.</p>
<pre><code class="language-javascript">import { useMutation, useQueryClient } from '@tanstack/react-query';
const usePostMutations = () => {
const queryClient = useQueryClient();
const baseConfig = {
onSuccess: () => {
// Revalidar após 5 segundos se o usuário não sair da página
queryClient.refetchQueries({
queryKey: ['posts'],
staleTime: 5 * 1000
});
},
onError: (error) => {
console.error('Erro na mutação:', error);
// Aqui você poderia enviar para um serviço de logging
},
};
const createMutation = useMutation({
mutationFn: async (newPost) => {
const response = await fetch('/api/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newPost),
});
return response.json();
},
...baseConfig,
});
const deleteMutation = useMutation({
mutationFn: async (postId) => {
const response = await fetch(/api/posts/${postId}, {
method: 'DELETE',
});
return response.json();
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['posts'], refetchType: 'active' });
},
});
return { createMutation, deleteMutation };
};</code></pre>
<p>Este padrão centraliza a lógica comum, evitando duplicação. Cada mutação especifica seu comportamento específico, enquanto compartilha configurações gerais.</p>
<h3>Sincronização com WebSockets</h3>
<p>Para aplicações em tempo real, você pode desacoplar mutações locais de atualizações do servidor recebidas através de WebSockets, mantendo cache sincronizado bidirecionalmente.</p>
<pre><code class="language-javascript">import { useEffect } from 'react';
import { useQueryClient } from '@tanstack/react-query';
const useSyncWithWebSocket = () => {
const queryClient = useQueryClient();
useEffect(() => {
const ws = new WebSocket('ws://seu-servidor.com');
ws.onmessage = (event) => {
const message = JSON.parse(event.data);
switch (message.type) {
case 'POST_CREATED':
// Um outro usuário criou um post
queryClient.invalidateQueries({ queryKey: ['posts'] });
break;
case 'POST_UPDATED':
// Atualiza diretamente no cache
queryClient.setQueryData(['posts', message.postId], message.data);
break;
case 'POST_DELETED':
// Remove do cache
queryClient.setQueryData(['posts'], (oldData) =>
Array.isArray(oldData)
? oldData.filter((post) => post.id !== message.postId)
: oldData
);
break;
default:
break;
}
};
return () => ws.close();
}, [queryClient]);
};
export default useSyncWithWebSocket;</code></pre>
<p>Este hook integra mudanças recebidas do servidor diretamente no cache do React Query. Quando um outro usuário atualiza dados, sua aplicação reflete isso imediatamente, sem necessidade de refetch manual.</p>
<h2>Conclusão</h2>
<p>Nesta jornada, você aprendeu que <strong>mutações no React Query não são apenas sobre enviar requisições, mas sobre orquestrar uma sincronização confiável entre cliente e servidor</strong>. O tripé invalidação-cache-estado garante que sua interface sempre reflita a realidade do servidor. Segundo, <strong>atualizações otimistas transformam a experiência do usuário</strong>, eliminando a percepção de latência mesmo em conexões lentas — o truque é sempre ter um plano B para reverter se algo der errado. Por fim, <strong>a granularidade oferecida pelo React Query permite estratégias sofisticadas de sincronização</strong>, desde invalidações precisas até sincronização em tempo real com WebSockets, tudo sem precisar gerenciar manualmente estado complexo. Use esses conceitos não como receitas, mas como ferramentas; entender quando cada padrão se aplica é o que separa código amador de código profissional.</p>
<h2>Referências</h2>
<ul>
<li><a href="https://tanstack.com/query/latest" target="_blank" rel="noopener noreferrer">React Query Official Documentation</a></li>
<li><a href="https://tanstack.com/query/latest/docs/react/guides/mutations" target="_blank" rel="noopener noreferrer">Mutations - TanStack Query</a></li>
<li><a href="https://tanstack.com/query/latest/docs/react/guides/important-defaults" target="_blank" rel="noopener noreferrer">Important Defaults - React Query</a></li>
<li><a href="https://tkdodo.eu/blog/react-query" target="_blank" rel="noopener noreferrer">Tkdodo's React Query Guide</a></li>
<li><a href="https://www.udemy.com/course/react-query-mastery/" target="_blank" rel="noopener noreferrer">Advanced Patterns with React Query</a></li>
</ul>
<p><!-- FIM --></p>