# Tanstack Query Expert

> Especialista em TanStack Query (React Query) — gerenciamento de estado assíncrono. Aborda fetching de dados, configuração de stale time, mutations, atualizações otimistas e integração com Next.js App Router (SSR).

- Skill: `artubss/tanstack-query-expert` (Agent Skill)
- Install (CLI): `npx skillmds@latest add artubss/tanstack-query-expert`
- Raw SKILL.md: https://api.skillmd.com/api/skills/artubss/tanstack-query-expert/raw
- Safety review: pending (external: skill-scanner PASS, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: artubss (https://skillmd.com/u/artubss)
- Updated: 2026-09-08
- Page: https://skillmd.com/skills/artubss/tanstack-query-expert

---


# Especialista em TanStack Query

Você é um especialista em TanStack Query (antigo React Query) em nível de produção. Você ajuda desenvolvedores a construir camadas robustas e performáticas de gerenciamento de estado assíncrono em aplicações React e Next.js. Você domina fetching de dados declarativo, invalidação de cache, atualizações otimistas de UI, sincronização em background, error boundaries e padrões de hidratação server-side rendering (SSR).

## Quando Usar Esta Habilidade

- Use ao configurar ou refatorar lógica de fetching de dados (substituindo `useEffect` + `useState`)
- Use ao projetar query keys (chaves baseadas em arrays, strictly typed)
- Use ao configurar comportamento global ou específico de `staleTime`, `gcTime` e `retry`
- Use ao escrever hooks `useMutation` para requisições POST/PUT/DELETE
- Use ao invalidar o cache (`queryClient.invalidateQueries`) após uma mutation
- Use ao implementar Atualizações Otimistas para feedback UX instantâneo
- Use ao integrar TanStack Query com Next.js App Router (Server Components + hidratação de Client Boundary)

## Conceitos Fundamentais

### Por que TanStack Query?

TanStack Query não é apenas para buscar dados; é um **gerenciador de estado assíncrono**. Ele lida com cache, atualizações em background, deduplicação de múltiplas requisições pelos mesmos dados, paginação e estados de loading/erro prontos para uso.

**Regra de Ouro:** Nunca use `useEffect` para buscar dados se TanStack Query está disponível no stack.

## Padrões de Definição de Queries

### O Padrão de Custom Hook (Melhor Prática)

Sempre abstraia chamadas de `useQuery` em custom hooks para encapsular a lógica de fetching, tipos TypeScript e query keys.

```typescript
import { useQuery } from '@tanstack/react-query';

// 1. Defina tipos rigorosos
type User = { id: string; name: string; status: 'active' | 'inactive' };

// 2. Defina a função fetcher
const fetchUser = async (userId: string): Promise<User> => {
  const res = await fetch(`/api/users/${userId}`);
  if (!res.ok) throw new Error('Failed to fetch user');
  return res.json();
};

// 3. Exporte um custom hook
export const useUser = (userId: string) => {
  return useQuery({
    queryKey: ['users', userId], // Query key baseada em array
    queryFn: () => fetchUser(userId),
    staleTime: 1000 * 60 * 5, // Dados frescos por 5 minutos (sem refetching em background)
    enabled: !!userId, // Query dependente: só executa se userId existe
  });
};
```

### Query Keys Avançadas

Query keys identificam exclusivamente o cache. Devem ser arrays, e a ordem importa.

```typescript
// Filtering / Sorting
useQuery({
  queryKey: ['issues', { status: 'open', sort: 'desc' }],
  queryFn: () => fetchIssues({ status: 'open', sort: 'desc' })
});

// Factory pattern para query keys (Altamente recomendado para aplicações grandes)
export const issueKeys = {
  all: ['issues'] as const,
  lists: () => [...issueKeys.all, 'list'] as const,
  list: (filters: string) => [...issueKeys.lists(), { filters }] as const,
  details: () => [...issueKeys.all, 'detail'] as const,
  detail: (id: number) => [...issueKeys.details(), id] as const,
};
```

## Mutations & Invalidação de Cache

### Mutation Básica com Invalidação

Quando você modifica dados no servidor, deve avisar ao client cache que os dados antigos estão obsoletos.

```typescript
import { useMutation, useQueryClient } from '@tanstack/react-query';

export const useCreatePost = () => {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: async (newPost: { title: string }) => {
      const res = await fetch('/api/posts', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(newPost),
      });
      return res.json();
    },
    // Na sucesso, invalide o cache 'posts' para dispararum refetch em background
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['posts'] });
    },
  });
};
```

### Atualizações Otimistas

Dê ao usuário feedback instantâneo atualizando o cache *antes* do servidor responder, e reverta se a requisição falhar.

```typescript
export const useUpdateTodo = () => {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: updateTodoFn,
    
    // 1. Disparado imediatamente quando mutate() é chamado
    onMutate: async (newTodo) => {
      // Cancele refetches pendentes para que não sobrescrevam nossa atualização otimista
      await queryClient.cancelQueries({ queryKey: ['todos'] });

      // Faça snapshot do valor anterior
      const previousTodos = queryClient.getQueryData(['todos']);

      // Atualize otimisticamente para o novo valor
      queryClient.setQueryData(['todos'], (old: any) => 
        old.map((todo: any) => todo.id === newTodo.id ? { ...todo, ...newTodo } : todo)
      );

      // Retorne um objeto context com o valor salvo em snapshot
      return { previousTodos };
    },
    
    // 2. Se a mutation falhar, use o context retornado de onMutate para reverter
    onError: (err, newTodo, context) => {
      queryClient.setQueryData(['todos'], context?.previousTodos);
    },
    
    // 3. Sempre refetch após erro ou sucesso para garantir sincronização com servidor
    onSettled: () => {
      queryClient.invalidateQueries({ queryKey: ['todos'] });
    },
  });
};
```

## Integração com Next.js App Router

### Inicializando o Provider

```typescript
// app/providers.tsx
'use client'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { useState } from 'react'

export default function Providers({ children }: { children: React.ReactNode }) {
  const [queryClient] = useState(
    () =>
      new QueryClient({
        defaultOptions: {
          queries: {
            staleTime: 60 * 1000, // 1 minuto
            refetchOnWindowFocus: false, // Previne refetching agressivo ao trocar de aba
          },
        },
      })
  )

  return (
    <QueryClientProvider client={queryClient}>
      {children}
    </QueryClientProvider>
  )
}
```

### Server Component Pre-fetching (Hidratação)

Faça pre-fetch de dados no servidor e passe-os ao cliente sem prop-drilling ou `initialData`.

```typescript
// app/posts/page.tsx (Server Component)
import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query';
import PostsList from './PostsList'; // Client Component

export default async function PostsPage() {
  const queryClient = new QueryClient();

  // Faça pre-fetch dos dados no servidor
  await queryClient.prefetchQuery({
    queryKey: ['posts'],
    queryFn: fetchPostsServerSide,
  });

  // Desidrate o cache e passe-o para a HydrationBoundary
  return (
    <HydrationBoundary state={dehydrate(queryClient)}>
      <PostsList />
    </HydrationBoundary>
  );
}
```

```typescript
// app/posts/PostsList.tsx (Client Component)
'use client'
import { useQuery } from '@tanstack/react-query';

export default function PostsList() {
  // Isso NÃO disparará uma requisição de rede ao montar!
  // Lê instantaneamente do cache desidratado do servidor.
  const { data } = useQuery({
    queryKey: ['posts'],
    queryFn: fetchPostsClientSide,
  });

  return <div>{data.map(post => <p key={post.id}>{post.title}</p>)}</div>;
}
```

## Melhores Práticas

- ✅ **Faça:** Crie factories de Query Key para que você não erre ao digitar `['users']` vs `['user']` em arquivos diferentes.
- ✅ **Faça:** Configure um `staleTime` global (ex: `1000 * 60`) se seus dados não mudam a cada segundo. O `staleTime` padrão é `0`, o que significa que TanStack Query dispara um refetch em background a cada remount do componente por padrão.
- ✅ **Faça:** Use `queryClient.setQueryData` com moderação. Geralmente é melhor apenas fazer `invalidateQueries` e deixar TanStack Query refetch os dados frescos organicamente.
- ✅ **Faça:** Abstraia todas as chamadas `useMutation` e `useQuery` em custom hooks. Views devem apenas dizer `const { mutate } = useCreatePost()`.
- ❌ **Não Faça:** Passe callbacks primitivos inline diretamente para `useQuery` sem memoização se você confia em closures. (Em vez disso, confie no array de dependências `queryKey`).
- ❌ **Não Faça:** Sincronize dados de query em estado React local (ex: `useEffect(() => setLocalState(data), [data])`). Use os dados de query diretamente. Se você precisa de estado derivado, derive-o durante o render.

## Troubleshooting

**Problema:** Loop infinito de fetching na aba de network.
**Solução:** Verifique seu `queryFn`. Se sua lógica de `fetch` não está estruturada corretamente, ou lança uma exceção não tratada antes de retornar, TanStack Query vai fazer retry automaticamente até 3 vezes (padrão). Se envolvido em um `useEffect` instável, faz loop infinito. Verifique `retry: false` para debug.

**Problema:** Confusão entre `staleTime` vs `gcTime` (antigo `cacheTime`).
**Solução:** `staleTime` governa quando um refetch em background é disparado. `gcTime` governa por quanto tempo os dados inativos ficam na memória após o componente desmontar. Se `gcTime` < `staleTime`, os dados serão deletados antes de ficarem obsoletos!
