React: TanStack Query (React Query)

Última atualização: 2026-08-26

Na última aula, Tom usou hooks personalizados e o axios para encapsular as solicitações HTTP, mas surgiram novos problemas: tanto o painel quanto a barra lateral exibem o número de usuários, e cada componente envia sua própria solicitação (desperdiçando largura de banda e recursos do servidor); quando um usuário altera seu nome de usuário na Página A, os dados na Página B permanecem desatualizados; e, após enviar um formulário de edição, o usuário precisa acionar manualmente uma atualização dos dados. Ele percebeu: É necessária uma solução de “gerenciamento de estado no lado do servidor” para tratar os dados da API como um tipo especial de estado — um que seja armazenado em cache, tenha um prazo de validade e possa ser sincronizado automaticamente.


1. O que você vai aprender



2. Diagramas conceituais

100%
flowchart TD
    A[useQuery Call] --> B{Cache Hit?}
    B -->|No matches found| C[Initiate API Request]
    B -->|Hit but Expired| C
    B -->|On Target and Fresh| D[Return cached data directly]
    C --> E[Cached Data]
    E --> F[Rendering Component]
    F --> G{staleTime Has it expired??}
    G -->|Not yet due| H[Data labeled as"Fresh"]
    G -->|Expired| I[Data labeled as"Expired"]
    I --> J{Bring the window back to the foreground?}
    J -->|is | C
    I --> K{refetchInterval?}
    K -->|is | C
    I --> L{There's something new useMutation<br/>invalidate?}
    L -->|is | C

    style A fill:#e1f5fe,stroke:#0288d1
    style E fill:#fff3e0,stroke:#f57c00
    style H fill:#e8f5e9,stroke:#388e3c
    style I fill:#ffcdd2,stroke:#d32f2f

O mecanismo central do TanStack Query: ler primeiro do cache → marcar como atualizado ou expirado → acionar automaticamente uma nova solicitação quando expirado.



3. Um cenário da vida real

O painel do Tom precisa exibir o número total de usuários, o número total de pedidos e uma lista dos pedidos recentes. Esses dados provêm de três APIs diferentes e devem ser mantidos atualizados em tempo real — quando um usuário modifica dados em outra página e retorna ao painel, ele deve ver os resultados mais recentes. Além disso, após o envio do formulário “Adicionar produto”, a lista de produtos deve ser atualizada automaticamente, sem a necessidade de atualizar manualmente a página.

(1) Problema: Gerenciar o cache manualmente é muito difícil

Sem o TanStack Query, Tom teve que cuidar disso sozinho:

JSX
// Manually Manage the Cache — You have to write similar logic for each component.
function Dashboard() {
  const [data, setData] = useState(null)
  const [loading, setLoading] = useState(true)

  useEffect(() => {
    fetch('/api/stats').then(res => res.json())
      .then(d => { setData(d); setLoading(false) })
      .catch(e => { setLoading(false) })
  }, [])

  // Question 1:Switch to another page and then come back → Resubmit Request(Waste!)
  // Question 2:If another component also needs the same data → Duplicate Request
  // Question 3:The data does not refresh automatically,Users may be seeing data from a few minutes ago
}
▶ Experimente

O TanStack Query resolve todos os problemas acima com um único hook useQuery.

Recurso Busca manual + useEffect Consulta no TanStack
Gerenciamento de cache Sem cache; os dados são perdidos ao serem removidos Armazenamento automático em cache; gerenciado pela queryKey
Solicitação duplicada Vários componentes usando os mesmos dados → Solicitações duplicadas Remover duplicatas automaticamente; solicitar apenas uma vez
Atualização automática Nenhuma Recuperação automática ao receber o foco da janela/ao se reconectar
Atualização do backend Nenhuma O parâmetro staleTime controla a atualização do backend
Status de carregamento/erro Carregamento/erro gerenciado manualmente Dados/carregamento/erro fornecidos automaticamente
Atualização otimista Implementação manual onMutate + onError Revertida
BASH
npm install @tanstack/react-query

(2) Inicializar o provedor

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

// Create QueryClient(Usually at app Entrance)
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 30 * 1000,      // Default 30 Data within seconds is considered"Fresh"
      cacheTime: 5 * 60 * 1000,  // Cache Retention 5 minutes
      retry: 3,                  // Failure retry 3 times
      refetchOnWindowFocus: true, // Refresh when the window returns to the foreground
    }
  }
})

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <YourApp />
    </QueryClientProvider>
  )
}
▶ Experimente

4. useQuery: Recuperação de dados

▶ Exemplo 1: Recuperação de dados do painel

JSX 📖 Somente leitura
import { useQuery } from '@tanstack/react-query'

// Encapsulated API Function
async function fetchDashboardStats() {
  const response = await fetch('/api/dashboard/stats')
  if (!response.ok) throw new Error('Failed to retrieve statistics')
  return response.json()
}

function Dashboard() {
  // useQuery One line of code replaced useState + useEffect + Manual Caching
  const {
    data: stats,        // Response Data
    isLoading,          // Loading for the first time(When there is no cache)
    isFetching,         // Is a request being made?(Including background retries)
    error,              // Error Object
    refetch             // Manually Trigger Retrieval
  } = useQuery({
    queryKey: ['dashboardStats'],   // Unique Identifier,Used for caching matches
    queryFn: fetchDashboardStats,   // Data Retrieval Functions
    staleTime: 30 * 1000,           // 30 Do not resend the request within seconds
    retry: 3,                       // Failure retry 3 times
  })

  if (isLoading) return <DashboardSkeleton />
  if (error) return <ErrorPanel message={error.message} onRetry={refetch} />

  return (
    <div className="dashboard">
      <StatCard title="Total Number of Users" value={stats.users} />
      <StatCard title="Total Number of Orders" value={stats.orders} />
      <StatCard title="Total Revenue" value={`$${stats.revenue}`} />
    </div>
  )
}

// The sidebar also displays the number of users — Using the same queryKey,No duplicate requests!
function Sidebar() {
  const { data: stats } = useQuery({
    queryKey: ['dashboardStats'],
    queryFn: fetchDashboardStats,
    staleTime: 30 * 1000,
  })

  return (
    <aside>
      <p>Users Online:{stats?.onlineUsers ?? '...'}</p>
    </aside>
  )
}
41 linhas de lógica (limite de 40, somente leitura)

Mecanismo-chave: Instâncias idênticas de queryKey compartilham o mesmo cache. O Painel e a Barra Lateral utilizam o mesmo ['dashboardStats'], e o TanStack Query deduplica automaticamente as solicitações — cada componente aciona apenas uma solicitação de API, e ambos os componentes são atualizados simultaneamente assim que os dados são retornados.

▶ Exemplo 2: Consultas com parâmetros

JSX
function ProductDetail({ productId }) {
  const { data, isLoading, error } = useQuery({
    queryKey: ['product', productId],    // queryKey Includes parameters
    queryFn: async () => {
      const res = await fetch(`/api/products/${productId}`)
      if (!res.ok) throw new Error('The product does not exist.')
      return res.json()
    },
    enabled: !!productId,    // productId Do not send a request if it is empty
    staleTime: 60 * 1000,
  })

  if (isLoading) return <p>Loading product details...</p>
  if (error) return <p>Error:{error.message}</p>

  return (
    <div>
      <h2>{data.name}</h2>
      <p className="price">${data.price}</p>
      <p>{data.description}</p>
    </div>
  )
}
▶ Experimente

O significado dos parâmetros incluídos em queryKey: O TanStack Query utiliza queryKey como um identificador único para o cache. ['product', 1] e ['product', 2] são dois caches distintos que não interferem um no outro. Quando productId muda de 1 para 2, o sistema prioriza a leitura de ['product', 2] do cache; se estiver armazenado em cache e não tiver expirado, ele é renderizado diretamente; caso contrário, ele envia uma solicitação.

▶ Exemplo: Campos-chave retornados por useQuery

Campo Significado Caso de uso
data Dados da última resposta bem-sucedida Exibir interface do usuário
isLoading Primeiro carregamento sem dados em cache Exibir a tela básica no primeiro carregamento
isFetching Quaisquer solicitações em andamento (incluindo novas tentativas em segundo plano) Exibir indicador de atualização em segundo plano
error Objeto de erro para solicitação com falha Exibir mensagem de erro
refetch Função para acionar manualmente uma nova solicitação Botão “Atualizar”
isStale Os dados estão desatualizados? Exibir avisos de atualização condicionalmente


5. useMutation: Gravação de dados

Use useQuery para leituras e useMutation para gravações. Essa é a regra de ouro do TanStack Query.

▶ Exemplo 3: Adicionar um produto e atualizar a lista

JSX 📖 Somente leitura
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'

// Product List Search
function useProducts() {
  return useQuery({
    queryKey: ['products'],
    queryFn: async () => {
      const res = await fetch('/api/products')
      return res.json()
    }
  })
}

function ProductManager() {
  const queryClient = useQueryClient()

  // Add a product mutation
  const addProductMutation = useMutation({
    mutationFn: async (newProduct) => {
      const res = await fetch('/api/products', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(newProduct)
      })
      if (!res.ok) throw new Error('Failed to add')
      return res.json()
    },
    // Steps to Take After Success:Invalidate the product list cache,Trigger a re-request
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['products'] })
      // Optional:Display a success message at the same time
      alert('Item added successfully!')
    },
    // Error Handling
    onError: (error) => {
      alert(`Failed to add:${error.message}`)
    }
  })

  // Data
  const { data: products, isLoading } = useProducts()

  // Form Submission
  function handleSubmit(event) {
    event.preventDefault()
    const formData = new FormData(event.target)
    const newProduct = {
      name: formData.get('name'),
      price: Number(formData.get('price'))
    }
    addProductMutation.mutate(newProduct)
    event.target.reset()
  }

  return (
    <div>
      <h2>Product Management</h2>

      <form onSubmit={handleSubmit}>
        <input name="name" placeholder="Product Name" required />
        <input name="price" type="number" placeholder="Price" required />
        <button type="submit" disabled={addProductMutation.isLoading}>
          {addProductMutation.isLoading ? 'Submitting......' : 'Add Item'}
        </button>
      </form>

      {addProductMutation.isError && (
        <p className="error">Submission Failed:{addProductMutation.error.message}</p>
      )}

      <hr />

      <h3>Product List</h3>
      {isLoading && <p>Loading......</p>}
      {products && (
        <ul>
          {products.map(p => (
            <li key={p.id}>{p.name} — ${p.price}</li>
          ))}
        </ul>
      )}
    </div>
  )
}
67 linhas de lógica (limite de 40, somente leitura)

Processo principal: useMutation.mutate() Aciona uma solicitação POST → O servidor processa a solicitação → onSuccess Executa invalidateQueries na chamada de retorno → O TanStack Query recupera automaticamente os dados de ['products'] → A interface de usuário da lista é atualizada automaticamente.

Por que não usar setData manualmente? Você pode chamar queryClient.setQueryData para atualizar manualmente o cache, mas uma abordagem melhor é fazer com que o TanStack Query solicite novamente os dados do servidor por meio de invalidateQueries — isso garante que os dados sempre venham do servidor e evita inconsistências entre o cache do front-end e os dados do lado do servidor.



6. Atualizações otimistas

Quando a latência da rede é alta, os usuários precisam aguardar a resposta do servidor após enviar uma solicitação antes de poderem ver as alterações na interface do usuário, o que resulta em uma experiência de usuário insatisfatória. A atualização otimista atualiza a interface do usuário imediatamente antes do envio da solicitação e reverte para o estado anterior caso a solicitação falhe.

▶ Exemplo 4: Alterando o status de conclusão da tarefa

JSX 📖 Somente leitura
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'

// Get the To-Do List
function useTodos() {
  return useQuery({
    queryKey: ['todos'],
    queryFn: async () => {
      const res = await fetch('/api/todos')
      return res.json()
    }
  })
}

function TodoList() {
  const queryClient = useQueryClient()
  const { data: todos } = useTodos()

  // Switch to "Completed" status — Using Optimistic Updates
  const toggleMutation = useMutation({
    // Actual API Request
    mutationFn: async ({ id, done }) => {
      const res = await fetch(`/api/todos/${id}`, {
        method: 'PATCH',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ done })
      })
      if (!res.ok) throw new Error('Update Failed')
      return res.json()
    },

    // ===== Optimistic Update Begins =====
    onMutate: async ({ id, done }) => {
      // 1. Cancel all ongoing todos Search(Avoid Overwriting Optimistic Updates)
      await queryClient.cancelQueries({ queryKey: ['todos'] })

      // 2. Save the current cache data,Used for rollback in case of failure
      const previousTodos = queryClient.getQueryData(['todos'])

      // 3. Refresh the cache immediately
      queryClient.setQueryData(['todos'], (old) =>
        old.map(todo =>
          todo.id === id ? { ...todo, done } : todo
        )
      )

      // 4. Restore old data for rollback
      return { previousTodos }
    },
    // ===== End of Optimistic Update =====

    // Rollback on Failure
    onError: (err, variables, context) => {
      if (context?.previousTodos) {
        queryClient.setQueryData(['todos'], context.previousTodos)
      }
    },

    // Whether we succeed or fail,Finally, resend the request to ensure synchronization with the server.
    onSettled: () => {
      queryClient.invalidateQueries({ queryKey: ['todos'] })
    }
  })

  return (
    <ul>
      {todos?.map(todo => (
        <li key={todo.id} style={{ opacity: toggleMutation.isLoading ? 0.7 : 1 }}>
          <input
            type="checkbox"
            checked={todo.done}
            onChange={() => toggleMutation.mutate({ id: todo.id, done: !todo.done })}
          />
          <span style={{ textDecoration: todo.done ? 'line-through' : 'none' }}>
            {todo.title}
          </span>
        </li>
      ))}
    </ul>
  )
}
59 linhas de lógica (limite de 40, somente leitura)

O guia de três passos para o otimismo:

  1. onMutate: Atualize o cache imediatamente antes de enviar a solicitação para que os usuários possam ver as alterações na hora; salve os dados antigos caso seja necessário reverter as alterações.
  2. onError: Restaurar o cache (revertê-lo) usando os dados históricos salvos quando uma solicitação falhar
  3. onSettled: Independentemente do sucesso ou do fracasso, solicite novamente os dados ao servidor para garantir consistência absoluta.


7. Estado do lado do servidor x Estado do lado do cliente

Para compreender o conceito por trás do TanStack Query, é necessário distinguir entre dois estados:

Dimensão Estado do servidor Estado do cliente
Fonte API de back-end / Banco de dados Front-end (local, ações do usuário)
Propriedade O servidor é o proprietário dos dados O front-end é o proprietário dos dados
Persistência Armazenado em um banco de dados Armazenado na memória ou no localStorage
Requisitos de sincronização Deve permanecer sincronizado com o servidor Não precisa permanecer sincronizado com o servidor
Método de atualização Gravar via API + Recarregar Usar diretamente setState
Ferramentas de gerenciamento TanStack Query State / Redux Toolkit
Exemplo Lista de usuários, dados de produtos, informações de pedidos Botão de alternância em janela pop-up, valores inseridos em formulários, cor do tema

Princípios Fundamentais:

▶ Exemplo: Consulta no TanStack com o DevTools

O TanStack Query oferece um componente dedicado do DevTools que permite visualizar o status do cache, o tempo de expiração e a hora da última atualização de todas as consultas durante o desenvolvimento.

BASH
npm install @tanstack/react-query-devtools
JSX
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <YourApp />
      {/* Display only in the development environment DevTools */}
      <ReactQueryDevtools initialIsOpen={false} />
    </QueryClientProvider>
  )
}
▶ Experimente

Recursos do DevTools:

  1. Visualizar todas as queryKeys e seus dados em cache correspondentes
  2. Visualizar os status “caducado”, “ativo” e “inativo” de cada entrada do cache
  3. Acionar manualmente as operações de recarga, invalidação e remoção
  4. Monitorar a latência da rede e o tamanho da resposta das solicitações

▶ Exemplo: Métodos globais do QueryClient

Além de useQuery e useMutation, o objeto queryClient também oferece vários métodos globais para manipular o cache de fora do componente:

JSX
import { queryClient } from './queryClient'

// 1. Invalidate the cache(Trigger a re-request)
queryClient.invalidateQueries({ queryKey: ['products'] })

// 2. Clear All Cache(When a user logs out)
queryClient.clear()

// 3. Set the default option(Global Changes staleTime)
queryClient.setDefaultOptions({
  queries: { staleTime: 60 * 1000 }
})

// 4. Retrieve Cached Data(Do not trigger a request)
const cachedData = queryClient.getQueryData(['products'])

// 5. Prefetch Data(User hover Preload when the link is reached)
queryClient.prefetchQuery({
  queryKey: ['product', '42'],
  queryFn: () => fetchProduct('42')
})
// After the user clicks the link,,The data is already in the cache.,Instant Rendering

// 6. Cancel the current query
queryClient.cancelQueries({ queryKey: ['product', '42'] })
▶ Experimente

A pré-busca de dados (prefetchQuery) é uma forma eficaz de melhorar a experiência do usuário. Quando o usuário passa o cursor sobre um link, uma solicitação é iniciada antecipadamente para que os dados já estejam carregados no momento em que ele clicar, proporcionando um efeito de “carregamento instantâneo”.

▶ Exemplo: Quando personalizar a configuração do QueryClient

O defaultOptions definido durante a inicialização estabelece o valor padrão global, mas cada chamada ao useQuery pode substituí-lo individualmente:

JSX
// Global Default:30 Freshness Period (in Seconds)
const queryClient = new QueryClient({
  defaultOptions: {
    queries: { staleTime: 30 * 1000, retry: 3 }
  }
})

// A Query Override:The data remains virtually unchanged,Set the shelf life to 10 minutes
function useProductCategories() {
  return useQuery({
    queryKey: ['categories'],
    queryFn: fetchCategories,
    staleTime: 10 * 60 * 1000,  // 10 Do not retry within minutes
  })
}

// Another query override:High real-time requirements,Disable Caching
function useRealtimeNotifications() {
  return useQuery({
    queryKey: ['notifications'],
    queryFn: fetchNotifications,
    staleTime: 0,               // The data expires immediately
    refetchInterval: 30 * 1000, // Auto-poll every 30s
  })
}
▶ Experimente

Princípios de configuração: Defina valores conservadores (intervalo mais curto staleTime) na configuração global e substitua-os individualmente para cada consulta, com base nas características dos dados. Para dados que mudam com frequência (notificações, estatísticas em tempo real), defina um staleTime curto ou até mesmo habilite a sondagem; para dados que mudam com pouca frequência (listas de categorias, informações de configuração), defina um staleTime longo para reduzir o número de solicitações.


❓ Perguntas Frequentes

P: Quais são as principais vantagens do TanStack Query em comparação com o uso de useEffect + fetch? R: Três vantagens principais: (1) Cache compartilhado — quando vários componentes utilizam o mesmo queryKey, as duplicatas são automaticamente filtradas para evitar solicitações redundantes; (2) Recarga automática — os dados são atualizados automaticamente quando a janela volta ao primeiro plano, a rede se reconecta ou durante a sondagem periódica; (3) Gerenciamento do ciclo de vida — tratamento automático dos estados de carregamento, erro e dados; novas tentativas automáticas em caso de falha; e não há necessidade de implementar manualmente um AbortController para cancelar solicitações. Um único useQuery substitui 20 linhas de useEffect + fetch + useState.

P: Qual é a diferença entre staleTime e cacheTime? O que acontece com staleTime = 0? R: staleTime controla a “atualidade” dos dados — dentro desse intervalo de tempo, os dados são considerados atualizados e não acionarão uma nova busca automática. cacheTime controla o tempo de retenção do cache — por quanto tempo o cache permanece após o componente ser desmontado antes de ser removido pela coleta de lixo. staleTime = 0 significa que os dados são marcados como expirados assim que são retornados, acionando uma nova busca em segundo plano toda vez que forem usados (embora os dados em cache sejam retornados primeiro e depois atualizados). Recomenda-se definir staleTime = 30s para evitar instabilidade nas solicitações.

P: No método onSuccess do useMutation, como devo escolher entre invalidateQueries e setQueryData? R: A opção preferida é invalidateQueries — invalidar o cache para forçar o TanStack Query a buscar novamente os dados do servidor, garantindo a consistência dos dados. setQueryData é adequado para cenários com pouquíssimas alterações e um formato de retorno conhecido (como a geração de um ID exclusivo no lado do cliente). Se você precisar exibir os resultados das ações do usuário imediatamente (sem esperar pela resposta do servidor), use atualizações otimistas (onMutate + rollback) em vez de setQueryData.

P: Quando vários componentes utilizam a mesma queryKey, como o TanStack Query determina quando acionar uma nova solicitação? R: As regras são: (1) Se houver acerto no cache e os dados não estiverem vencidos (dentro do staleTime), os dados em cache são retornados diretamente, sem o envio de uma solicitação; (2) Se houver acerto no cache, mas o cache estiver expirado, o resultado em cache é retornado imediatamente e uma nova solicitação é iniciada em segundo plano; (3) Se não houver cache, uma solicitação é iniciada. Independentemente de quantos componentes estejam assinados no mesmo queryKey, apenas uma solicitação será enviada.

P: O TanStack Query e o Zustand podem ser usados juntos? R: Sim, e essa abordagem é recomendada. O TanStack Query gerencia o estado do lado do servidor (dados da API), enquanto o Zustand gerencia o estado do lado do cliente (estado da interface do usuário). Uma arquitetura comum: o TanStack Query busca e armazena os dados em cache → injeta os dados no armazenamento do Zustand para processamento posterior → os componentes leem o estado final do Zustand. Os dois são complementares, não substitutos um do outro.


📖 Resumo


📝 Exercícios

  1. Crie um componente de lista de usuários usando useQuery: utilize https://jsonplaceholder.typicode.com/users como API, implemente o compartilhamento de cache (ambos os componentes usam o mesmo queryKey para garantir que a solicitação seja enviada apenas uma vez) e adicione um botão de atualização manual.
  2. Use useMutation para implementar o recurso “Adicionar produto”: quando o formulário é enviado, ele aciona uma solicitação POST; se for bem-sucedido, a lista de produtos é atualizada automaticamente; se não for bem-sucedido, é exibida uma mensagem de erro.
  3. Implemente uma atualização otimista para “alterar o status de conclusão de uma tarefa”: ao clicar na caixa de seleção, o status é alterado imediatamente; se a solicitação à API falhar, a alteração é revertida. Depois de escrever o código, teste-o desconectando-se da rede e clicando na caixa de seleção para observar o comportamento de reversão.
Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%