TanStack Query: o estado assíncrono que mudou como apps React buscam dados

1. O Problema Clássico de Gerenciamento de Estado Assíncrono no React

1.1 A lacuna entre estado síncrono (Redux, Context) e dados remotos

Durante anos, desenvolvedores React trataram dados de API como se fossem estado local. Ferramentas como Redux e Context API foram projetadas para estado síncrono — temas, formulários, preferências do usuário. Quando aplicadas a dados remotos, criavam uma camada de complexidade desnecessária: actions, reducers, middlewares para chamadas assíncronas, tudo para gerenciar algo que o navegador já faz bem (cache HTTP).

1.2 Boilerplate e complexidade: loading, error, refetch, cache manual

Cada requisição exigia gerenciar manualmente quatro estados:

const [data, setData] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);

useEffect(() => {
  setIsLoading(true);
  fetch('/api/users')
    .then(res => res.json())
    .then(setData)
    .catch(setError)
    .finally(() => setIsLoading(false));
}, []);

Esse padrão se repetia dezenas de vezes por aplicação. Sem cache, sem refetch automático, sem deduplicação de requisições.

1.3 Por que useEffect + useState não escala para chamadas de API

Além do boilerplate, useEffect introduz race conditions, memory leaks (se o componente desmontar antes da resposta) e não oferece sincronização entre componentes que consomem o mesmo dado. Cada componente faz sua própria requisição, duplicando tráfego e degradando performance.

2. TanStack Query: Filosofia e Conceitos Fundamentais

2.1 Estado do servidor vs. estado do cliente — a separação essencial

TanStack Query introduz uma distinção crucial: dados no servidor (usuários, posts, configurações) não pertencem ao React. São um cache local de uma fonte remota. O React não deve "possuir" esses dados, apenas espelhá-los.

2.2 Query Keys e Query Functions: identificação e busca de dados

Cada consulta é identificada por uma chave única (array) e uma função que retorna uma Promise:

const { data, isLoading } = useQuery({
  queryKey: ['users', { page: 1 }],
  queryFn: () => fetch('/api/users?page=1').then(res => res.json())
});

2.3 Cache inteligente, stale-while-revalidate e background refetch

TanStack Query implementa o padrão stale-while-revalidate: dados em cache são exibidos imediatamente, enquanto uma revalidação em background atualiza o cache. Se o usuário navegar para uma tela e voltar, os dados aparecem instantaneamente do cache, sem loader.

3. Configuração e Uso Básico em Projetos React

3.1 Instalação e setup do QueryClientProvider

npm install @tanstack/react-query
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

const queryClient = new QueryClient();

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <UserList />
    </QueryClientProvider>
  );
}

3.2 Exemplo prático: useQuery para buscar uma lista de usuários

function UserList() {
  const { data, isLoading, isError, error } = useQuery({
    queryKey: ['users'],
    queryFn: () => fetch('/api/users').then(res => res.json()),
  });

  if (isLoading) return <p>Carregando...</p>;
  if (isError) return <p>Erro: {error.message}</p>;

  return (
    <ul>
      {data.map(user => <li key={user.id}>{user.name}</li>)}
    </ul>
  );
}

3.3 Tratamento de estados: isLoading, isError, data e error

TanStack Query expõe estados granulares: isLoading (primeira carga), isFetching (qualquer requisição, incluindo revalidação), isError, isSuccess. Isso permite UIs precisas sem lógica condicional complexa.

4. Mutations: Criando, Atualizando e Deletando Dados com TanStack Query

4.1 useMutation e o ciclo de vida de operações de escrita

const mutation = useMutation({
  mutationFn: (newUser) => fetch('/api/users', {
    method: 'POST',
    body: JSON.stringify(newUser),
  }),
  onSuccess: () => {
    queryClient.invalidateQueries({ queryKey: ['users'] });
  },
});

4.2 Invalidando queries e atualizando cache após mutações

Após criar um usuário, invalidar a query ['users'] força uma revalidação automática. Alternativamente, é possível atualizar o cache diretamente com setQueryData, evitando requisições desnecessárias.

4.3 Otimistic updates: melhorando a UX com respostas instantâneas

const mutation = useMutation({
  mutationFn: updateUser,
  onMutate: async (updatedUser) => {
    await queryClient.cancelQueries({ queryKey: ['users'] });
    const previous = queryClient.getQueryData(['users']);
    queryClient.setQueryData(['users'], old => 
      old.map(u => u.id === updatedUser.id ? updatedUser : u)
    );
    return { previous };
  },
  onError: (err, newUser, context) => {
    queryClient.setQueryData(['users'], context.previous);
  },
});

5. Estratégias Avançadas de Cache e Performance

5.1 Configuração de staleTime e cacheTime para diferentes cenários

  • staleTime: tempo até o dado ser considerado obsoleto (default: 0, sempre refetch ao montar)
  • cacheTime: tempo até o dado ser removido do cache após não ser usado (default: 5 minutos)

Para dados raramente alterados (lista de países), staleTime: 10 * 60 * 1000 evita requisições desnecessárias.

5.2 Paginação e infinite queries com useInfiniteQuery

const { data, fetchNextPage, hasNextPage } = useInfiniteQuery({
  queryKey: ['users'],
  queryFn: ({ pageParam = 1 }) => fetch(`/api/users?page=${pageParam}`),
  getNextPageParam: (lastPage) => lastPage.nextPage ?? undefined,
});

5.3 Prefetching e query hydration para SSR/SSG (Next.js)

// Em getServerSideProps
const queryClient = new QueryClient();
await queryClient.prefetchQuery(['users'], fetchUsers);

return {
  props: {
    dehydratedState: dehydrate(queryClient),
  },
};

6. Padrões de Arquitetura e Organização de Queries

6.1 Custom hooks encapsulando queries e mutations por domínio

function useUsers() {
  return useQuery({
    queryKey: ['users'],
    queryFn: fetchUsers,
  });
}

function useCreateUser() {
  const queryClient = useQueryClient();
  return useMutation({
    mutationFn: createUser,
    onSuccess: () => queryClient.invalidateQueries(['users']),
  });
}

6.2 Separando lógica assíncrona da UI com hooks tipados

Cada domínio (users, posts, comments) tem seu próprio arquivo de hooks. A UI chama hooks, nunca lida diretamente com fetch.

6.3 Testabilidade: mockando queries e mutations em testes unitários

import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

const queryClient = new QueryClient({
  defaultOptions: { queries: { retry: false } },
});

function TestWrapper({ children }) {
  return (
    <QueryClientProvider client={queryClient}>
      {children}
    </QueryClientProvider>
  );
}

7. Casos de Uso Reais e Comparação com Alternativas

7.1 Exemplo completo: dashboard com múltiplas queries dependentes

function Dashboard() {
  const { data: user } = useQuery(['user', userId], () => fetchUser(userId));
  const { data: orders } = useQuery({
    queryKey: ['orders', user?.id],
    enabled: !!user?.id,
  });
  // orders só busca quando user estiver disponível
}

7.2 TanStack Query vs. RTK Query vs. SWR — quando escolher cada um

  • TanStack Query: mais flexível, suporte a mutations complexas, optimistic updates, infinite queries
  • RTK Query: integração nativa com Redux, ideal para apps que já usam Redux
  • SWR: mais simples, foco em revalidação automática, menor curva de aprendizado

7.3 Limitações e armadilhas comuns

  • Chaves mal definidas: usar objetos não serializáveis como chave causa loops infinitos
  • Excesso de queries: cada filtro ou ordenação deve ser um parâmetro na queryKey, não uma nova query
  • Cache desatualizado: esquecer de invalidar queries após mutations leva a dados obsoletos

8. Conclusão: Impacto no Ecossistema React e Tendências Futuras

8.1 Como TanStack Query mudou a mentalidade de gerenciamento de estado

TanStack Query popularizou a ideia de que estado do servidor merece tratamento especial. Reduziu drasticamente boilerplate, eliminou race conditions e tornou o cache algo trivial.

8.2 Integração com React Suspense e Server Components

Com React 18+, TanStack Query suporta Suspense, permitindo que componentes "suspendam" enquanto dados carregam. Em Server Components, o prefetching se torna ainda mais natural.

8.3 O futuro: estado assíncrono nativo no React e o papel do TanStack Query

React está explorando hooks nativos para dados assíncronos (use()). TanStack Query, no entanto, continuará relevante por oferecer cache, mutations, optimistic updates e infinite queries — funcionalidades que vão além do escopo do React core.


Referências