Entendendo o Cache e sua Importância no React Query
O cache é o coração do React Query. Diferentemente de outras bibliotecas que apenas gerenciam estado, o React Query mantém uma cópia dos dados já fetched no navegador, evitando requisições desnecessárias. Quando você faz uma query pela primeira vez, a resposta é armazenada em memória com uma chave única. Na próxima vez que essa query for necessária, o React Query retorna os dados do cache instantaneamente, sem fazer uma nova requisição HTTP.
Para entender isso na prática, vamos ver como o cache funciona por padrão. Quando você cria uma query com useQuery, ela passa por diferentes estados: isLoading (primeira requisição), isFetching (busca em background), e finalmente retorna dados do cache. O cache também tem um conceito de "stale" (obsoleto), que é quando os dados precisam ser revalidados com o servidor.
import { useQuery } from '@tanstack/react-query';
const UserProfile = ({ userId }: { userId: string }) => {
const { data, isLoading, isFetching } = useQuery({
queryKey: ['user', userId],
queryFn: async () => {
const response = await fetch(`/api/users/${userId}`);
return response.json();
},
staleTime: 5 * 60 * 1000, // 5 minutos
});
if (isLoading) return <div>Carregando...</div>;
return (
<div>
<h1>{data.name}</h1>
{isFetching && <span>(atualizando...)</span>}
</div>
);
};
Neste exemplo, após os primeiros 5 minutos (staleTime), os dados são considerados obsoletos. Mas observe: os dados ainda estão no cache, apenas marcados como stale. Se o usuário navegar para outra página e voltar dentro da janela de cacheTime, os dados serão mostrados imediatamente enquanto uma nova requisição é feita em background.
Dominando Stale Time e Cache Time
Stale Time e Cache Time são dois conceitos distintos que precisam ser bem compreendidos. Stale Time define quanto tempo os dados são considerados "frescos". Enquanto os dados não estão stale, nenhuma requisição é feita em background. Cache Time (renomeado para gcTime no TanStack Query v5) define por quanto tempo os dados permanecem na memória após não serem mais utilizados.
A configuração padrão do React Query é staleTime: 0 e gcTime: 5 * 60 * 1000 (5 minutos). Isso significa que os dados são imediatamente considerados obsoletos, mas permanecem na memória por 5 minutos após o último acesso. Se um usuário navegar para a página de perfil, depois para home, e retornar ao perfil dentro de 5 minutos, os dados serão mostrados do cache e uma requisição silenciosa será feita em background.
import { useQuery, useQueryClient } from '@tanstack/react-query';
const ProductList = () => {
const queryClient = useQueryClient();
const { data: products } = useQuery({
queryKey: ['products'],
queryFn: async () => {
const response = await fetch('/api/products');
return response.json();
},
staleTime: 10 * 60 * 1000, // 10 minutos - dados ficam frescos por 10 min
gcTime: 15 * 60 * 1000, // 15 minutos - cache permanece por 15 min
});
const handleRefreshManual = () => {
// Força revalidação imediata
queryClient.invalidateQueries({ queryKey: ['products'] });
};
return (
<div>
<button onClick={handleRefreshManual}>Atualizar dados</button>
{/* renderizar produtos */}
</div>
);
};
A estratégia correta depende do seu caso de uso. Para dados que mudam frequentemente (como notificações ou preços), use um staleTime menor. Para dados mais estáticos (como informações de um artigo), use um staleTime maior. O gcTime deve ser sempre maior que o staleTime, caso contrário o cache será limpo antes dos dados serem considerados stale.
Invalidação Manual e Revalidação Inteligente
Às vezes, você sabe que os dados ficaram obsoletos e precisa invalidá-los manualmente. O queryClient.invalidateQueries() marca queries como stale, forçando uma revalidação quando o componente tentar acessá-las. Isso é diferente de deletar o cache: os dados continuam disponíveis enquanto a nova requisição é feita.
import { useMutation, useQueryClient } from '@tanstack/react-query';
const CreatePost = () => {
const queryClient = useQueryClient();
const createPostMutation = useMutation({
mutationFn: async (newPost: { title: string; content: string }) => {
const response = await fetch('/api/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newPost),
});
return response.json();
},
onSuccess: (data) => {
// Invalida a lista de posts para que seja recarregada
queryClient.invalidateQueries({ queryKey: ['posts'] });
// Ou atualiza o cache diretamente (mais eficiente)
queryClient.setQueryData(['posts'], (oldData: any) => [
...oldData,
data,
]);
},
});
return (
<button onClick={() => createPostMutation.mutate({ title: '', content: '' })}>
Criar Post
</button>
);
};
Prefetch: Carregando Dados Antes da Necessidade
Prefetch é uma técnica poderosa para melhorar a experiência do usuário. Ao invés de esperar o usuário clicar em um link ou navegar para uma página, você antecipa quais dados serão necessários e os carrega em background. Isso é particularmente útil em paginação, listagens onde o usuário provavelmente vai clicar no próximo item, ou em previsão de navegação.
O React Query fornece o queryClient.prefetchQuery() que faz uma requisição sem renderizar dados, apenas armazenando no cache. Se um componente depois usar useQuery com a mesma chave, os dados já estarão disponíveis.
import { useQueryClient } from '@tanstack/react-query';
import { useEffect } from 'react';
const ProductListWithPrefetch = ({ currentPage }: { currentPage: number }) => {
const queryClient = useQueryClient();
// Prefetch da próxima página quando a página atual carregar
useEffect(() => {
queryClient.prefetchQuery({
queryKey: ['products', currentPage + 1],
queryFn: async () => {
const response = await fetch(`/api/products?page=${currentPage + 1}`);
return response.json();
},
staleTime: 5 * 60 * 1000,
});
}, [currentPage, queryClient]);
// Query da página atual
const { data: products } = useQuery({
queryKey: ['products', currentPage],
queryFn: async () => {
const response = await fetch(`/api/products?page=${currentPage}`);
return response.json();
},
});
return (
<div>
{/* renderizar produtos */}
</div>
);
};
Uma implementação mais sofisticada é prefetch em hover. Quando o usuário passa o mouse sobre um link, você já carrega os dados que ele provavelmente vai ver:
import { useQueryClient } from '@tanstack/react-query';
const UserLink = ({ userId }: { userId: string }) => {
const queryClient = useQueryClient();
const handleMouseEnter = () => {
queryClient.prefetchQuery({
queryKey: ['user', userId],
queryFn: async () => {
const response = await fetch(`/api/users/${userId}`);
return response.json();
},
staleTime: 10 * 60 * 1000,
});
};
return (
<a
href={`/users/${userId}`}
onMouseEnter={handleMouseEnter}
>
Ver Perfil
</a>
);
};
Optimistic UI: Atualizações Antes da Confirmação do Servidor
Optimistic UI é quando você atualiza a interface do usuário imediatamente, antes de confirmar com o servidor que a operação foi bem-sucedida. Isso cria uma experiência mais rápida e responsiva. Se algo der errado, você reverte a mudança.
O React Query facilitou isso com as callbacks onMutate, onSuccess e onError do useMutation. A estratégia típica é: quando o usuário submete uma ação, você atualiza o cache otimisticamente, faz a requisição, e se falhar, reverte.
import { useMutation, useQueryClient } from '@tanstack/react-query';
interface Post {
id: string;
title: string;
content: string;
}
const EditPost = ({ postId, initialPost }: { postId: string; initialPost: Post }) => {
const queryClient = useQueryClient();
const editPostMutation = useMutation({
mutationFn: async (updatedPost: Post) => {
const response = await fetch(`/api/posts/${postId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(updatedPost),
});
if (!response.ok) {
throw new Error('Falha ao atualizar post');
}
return response.json();
},
onMutate: async (newPost) => {
// Cancela qualquer query em andamento para evitar sobrescrita
await queryClient.cancelQueries({ queryKey: ['post', postId] });
// Salva os dados antigos em caso de rollback
const previousPost = queryClient.getQueryData<Post>(['post', postId]);
// Atualiza o cache otimisticamente
queryClient.setQueryData(['post', postId], newPost);
// Retorna o contexto para usar em onError
return { previousPost };
},
onSuccess: (data) => {
// Sucesso confirmado - dados já estão corretos no cache
console.log('Post atualizado com sucesso:', data);
},
onError: (error, variables, context) => {
// Se algo der errado, reverte para os dados antigos
if (context?.previousPost) {
queryClient.setQueryData(['post', postId], context.previousPost);
}
console.error('Erro ao atualizar post:', error);
},
});
const handleSave = (updatedPost: Post) => {
editPostMutation.mutate(updatedPost);
};
return (
<div>
{editPostMutation.isPending && <span>Salvando...</span>}
{editPostMutation.isError && <span>Erro ao salvar</span>}
{/* Formulário aqui */}
</div>
);
};
Um exemplo ainda mais realista é quando você está atualizando uma lista e precisa refletir a mudança imediatamente:
import { useMutation, useQueryClient } from '@tanstack/react-query';
interface Task {
id: string;
title: string;
completed: boolean;
}
const TaskToggle = ({ task }: { task: Task }) => {
const queryClient = useQueryClient();
const toggleTaskMutation = useMutation({
mutationFn: async (completed: boolean) => {
const response = await fetch(`/api/tasks/${task.id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ completed }),
});
return response.json();
},
onMutate: async (newCompleted) => {
// Cancela requisições simultâneas
await queryClient.cancelQueries({ queryKey: ['tasks'] });
// Salva estado anterior
const previousTasks = queryClient.getQueryData<Task[]>(['tasks']);
// Atualiza cache otimisticamente
queryClient.setQueryData(['tasks'], (oldTasks: Task[]) =>
oldTasks.map((t) =>
t.id === task.id ? { ...t, completed: newCompleted } : t
)
);
return { previousTasks };
},
onError: (error, variables, context) => {
// Reverte em caso de erro
if (context?.previousTasks) {
queryClient.setQueryData(['tasks'], context.previousTasks);
}
},
});
return (
<input
type="checkbox"
checked={task.completed}
onChange={(e) => toggleTaskMutation.mutate(e.target.checked)}
disabled={toggleTaskMutation.isPending}
/>
);
};
A chave do Optimistic UI é sempre ter um plano de reversão. Se você não conseguir reverter facilmente, não use essa técnica, pois pode deixar o usuário com dados inconsistentes.
Conclusão
Os três pilares avançados do React Query que dominamos neste artigo — Cache inteligente com Stale Time, Prefetch para antecipar dados, e Optimistic UI para feedback imediato — transformam a experiência do usuário de uma forma que é invisível mas profundamente sentida. Você não apenas reduz requisições desnecessárias, mas cria interfaces que parecem instantâneas mesmo com conexões lentas.
A regra prática que deve guiar suas decisões: configure um staleTime apropriado para quantas vezes seus dados mudam (dados estáticos = staleTime alto), use prefetchQuery() quando puder antecipar onde o usuário vai clicar, e implemente Optimistic UI apenas em ações onde você pode reverter com segurança. O React Query fornece todos os mecanismos; a verdadeira maestria está em saber quando e como usá-los.