Guia Completo de React Query Avançado: Cache, Stale Time, Prefetch e Optimistic UI Já leu

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 , ela passa por diferentes estados: (primeira requisição), (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. /api/users/${userId} Neste exemplo, após os primeiros 5 minutos ( ), os dados são considerados obsoletos. Mas observe: os dados ainda estão no cache,

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.

Referências


Artigos relacionados