Guias & Tutoriais

Gerenciamento de Estado de Servidor com TanStack Query: Guia Prático

Aprenda a usar o TanStack Query (anteriormente React Query) para gerenciar buscas de dados, cache, atualizações e sincronização em aplicações React, simplificando o gerenciamento de estado de servidor. Este guia cobre desde a configuração básica, useQuery, useMutation, até configurações avançadas, paginação e boas práticas para otimizar seu app.

Gerenciamento de Estado de Servidor com TanStack Query: Guia Prático

Aprenda a usar o TanStack Query para gerenciar buscas de dados, cache, atualizações e sincronização em aplicações React, simplificando o gerenciamento de estado de servidor.


Introdução ao TanStack Query

TanStack Query (anteriormente React Query) é uma biblioteca poderosa para gerenciar o estado de servidor em aplicações React. Ela lida com busca, cache, sincronização e atualização de dados, permitindo que você se concentre na lógica da sua aplicação.

Analogia: Imagine que seu aplicativo React é um restaurante. TanStack Query é como o gerente de cozinha que:

  • Sabe quais pratos estão disponíveis (cache)

  • Verifica se os ingredientes estão frescos (staleTime)

  • Solicita novos ingredientes quando necessário (refetch)

  • Lida com pedidos especiais (mutations)

  • Notifica quando algo está esgotado (erro)

Conceito Fundamental 1: Configuração Básica

Primeiro, precisamos configurar o QueryClient e envolver nossa aplicação:

JSX
// src/main.jsximport React from 'react';import ReactDOM from 'react-dom/client';import { QueryClient, QueryClientProvider } from '@tanstack/react-query';import App from './App';// Cria uma instância do QueryClientconst queryClient = new QueryClient({  defaultOptions: {    queries: {      staleTime: 1000 * 60 * 5, // 5 minutos      refetchOnWindowFocus: false,    },  },});ReactDOM.createRoot(document.getElementById('root')).render(  <React.StrictMode>    <QueryClientProvider client={queryClient}>      <App />    </QueryClientProvider>  </React.StrictMode>);

Explicação:

  • QueryClient gerencia o cache e as operações

  • QueryClientProvider disponibiliza o client para todos os componentes

  • defaultOptions define configurações globais

Dica: Mantenha uma única instância do QueryClient para toda a aplicação.

Conceito Fundamental 2: Buscando Dados com useQuery

O hook useQuery é usado para buscar e sincronizar dados:

JSX
// src/components/UserList.jsximport { useQuery } from '@tanstack/react-query';const fetchUsers = async () => {  const response = await fetch('https://api.example.com/users');  if (!response.ok) {    throw new Error('Falha ao buscar usuários');  }  return response.json();};export const UserList = () => {  const { data, error, isLoading, isError } = useQuery({    queryKey: ['users'], // Chave única para identificar esta query    queryFn: fetchUsers, // Função que busca os dados  });  if (isLoading) {    return <div>Carregando...</div>;  }  if (isError) {    return <div>Erro: {error.message}</div>;  }  return (    <div>      <h1>Usuários</h1>      <ul>        {data.map(user => (          <li key={user.id}>{user.name}</li>        ))}      </ul>    </div>  );};

Explicação:

  • queryKey: Identificador único para esta query (organiza o cache)

  • queryFn: Função assíncrona que retorna os dados ou lança erro

  • isLoading: Indica se a query está carregando pela primeira vez

  • isError: Indica se ocorreu um erro

  • error: Objeto de erro se isError for true

  • data: Os dados retornados pela query

Resultado esperado:

  • Inicialmente exibe "Carregando..."

  • Após a busca, exibe uma lista de usuários ou mensagem de erro

Conceito Fundamental 3: Atualizando Dados com useMutation

useMutation é usado para criar, atualizar ou deletar dados:

JSX
// src/components/AddUser.jsximport { useMutation, useQueryClient } from '@tanstack/react-query';const addUser = async (newUser) => {  const response = await fetch('https://api.example.com/users', {    method: 'POST',    headers: {      'Content-Type': 'application/json',    },    body: JSON.stringify(newUser),  });  if (!response.ok) {    throw new Error('Falha ao adicionar usuário');  }  return response.json();};export const AddUser = () => {  const queryClient = useQueryClient();  const [name, setName] = useState('');  const mutation = useMutation({    mutationFn: addUser,    onSuccess: () => {      // Invalida a query de usuários para forçar refetch      queryClient.invalidateQueries({ queryKey: ['users'] });    },  });  const handleSubmit = (e) => {    e.preventDefault();    mutation.mutate({ name });    setName('');  };  return (    <form onSubmit={handleSubmit}>      <input        type="text"        value={name}        onChange={(e) => setName(e.target.value)}        placeholder="Nome do usuário"      />      <button type="submit" disabled={mutation.isLoading}>        {mutation.isLoading ? 'Adicionando...' : 'Adicionar Usuário'}      </button>      {mutation.isError && <div>Erro: {mutation.error.message}</div>}    </form>  );};

Explicação:

  • useQueryClient: Acessa o cliente para invalidar queries

  • mutationFn: Função que executa a mutação

  • onSuccess: Callback executado quando a mutação é bem-sucedida

  • mutate: Função para acionar a mutação

  • isLoading: Indica se a mutação está em andamento

Conceito Avançado: Configurações de Query

TanStack Query oferece muitas configurações para controlar o comportamento:

JSX
const { data } = useQuery({  queryKey: ['posts'],  queryFn: fetchPosts,  // Dados são considerados frescos por 5 minutos  staleTime: 1000 * 60 * 5,  // Dados são mantidos no cache por 10 minutos após ficarem obsoletos  cacheTime: 1000 * 60 * 10,  // Refetch quando a janela ganhar foco  refetchOnWindowFocus: true,  // Refetch quando a conexão for restabelecida  refetchOnReconnect: true,  // Refetch quando a página for retomada  refetchOnMount: true,  // Busca em segundo plano (a cada 30 segundos)  refetchInterval: 1000 * 30,  // Desativa busca em segundo plano quando a aba estiver inativa  refetchIntervalInBackground: false,});

Exemplo Prático 1: Paginação

JSX
// src/components/PaginatedPosts.jsximport { useQuery } from '@tanstack/react-query';const fetchPosts = async (page = 1, limit = 10) => {  const response = await fetch(    `https://api.example.com/posts?page=${page}&limit=${limit}`  );  if (!response.ok) {    throw new Error('Falha ao buscar posts');  }  return response.json();};export const PaginatedPosts = () => {  const [page, setPage] = useState(1);  const { data, isLoading, isError } = useQuery({    queryKey: ['posts', page], // Inclui a página na chave    queryFn: () => fetchPosts(page),    keepPreviousData: true, // Mantém dados da página anterior  });  if (isLoading) return <div>Carregando...</div>;  if (isError) return <div>Erro ao carregar posts</div>;  return (    <div>      <h1>Posts</h1>      <ul>        {data.posts.map(post => (          <li key={post.id}>{post.title}</li>        ))}      </ul>      <div>        <button onClick={() => setPage(page - 1)} disabled={page === 1}>          Anterior        </button>        <span> Página {page} </span>        <button onClick={() => setPage(page + 1)}>          Próximo        </button>      </div>    </div>  );};

Exemplo Prático 2: Busca Infinita

JSX
// src/components/InfinitePosts.jsximport { useInfiniteQuery } from '@tanstack/react-query';const fetchPosts = async ({ pageParam = 1 }) => {  const response = await fetch(    `https://api.example.com/posts?page=${pageParam}`  );  if (!response.ok) {    throw new Error('Falha ao buscar posts');  }  return response.json();};export const InfinitePosts = () => {  const {    data,    fetchNextPage,    hasNextPage,    isFetchingNextPage,    status,  } = useInfiniteQuery({    queryKey: ['infinite-posts'],    queryFn: fetchPosts,    getNextPageParam: (lastPage) => lastPage.nextPage,    initialPageParam: 1,  });  if (status === 'loading') return <div>Carregando...</div>;  if (status === 'error') return <div>Erro ao carregar posts</div>;  return (    <div>      <h1>Posts Infinitos</h1>      <div>        {data.pages.map((page, i) => (          <div key={i}>            {page.posts.map(post => (              <div key={post.id}>{post.title}</div>            ))}          </div>        ))}      </div>      <button        onClick={() => fetchNextPage()}        disabled={!hasNextPage || isFetchingNextPage}      >        {isFetchingNextPage          ? 'Carregando mais...'          : hasNextPage          ? 'Carregar Mais'          : 'Nada mais para carregar'}      </button>    </div>  );};

Erros Comuns e Como Evitar

  1. Não invalidar queries após mutations

    • Problema: Os dados exibidos não refletem as alterações recentes

    • Solução: Use onSuccess em useMutation para invalidar queries relevantes

  2. Chaves de query inconsistentes

    • Problema: O mesmo endpoint com parâmetros diferentes é tratado como a mesma query

    • Solução: Inclua todos os parâmetros relevantes na queryKey

  3. Esquecer de tratar estados de erro

    • Problema: A aplicação quebra quando uma falha de rede ocorre

    • Solução: Sempre verifique isError e error em seus componentes

  4. Usar staleTime muito curto

    • Problema: Muitas requisições desnecessárias ao servidor

    • Solução: Ajuste o staleTime com base na frequência de atualização dos dados

Boas Práticas

  • Use chaves de query específicas e previsíveis

  • Mantenha as funções de busca puras e sem efeitos colaterais

  • Trate todos os estados possíveis (loading, error, success)

  • Configure staleTime e cacheTime apropriadamente para seu caso de uso

  • Use select para transformar dados no componente quando necessário

  • Prefira invalidar queries em vez de refetch manualmente

  • Documente suas chaves de query para facilitar a manutenção

Para ir além

Takeaway: TanStack Query simplifica drasticamente o gerenciamento de estado de servidor em React. Ao entender os conceitos fundamentais de queries, mutations e cache, você pode criar aplicações mais responsivas, resilientes e fáceis de manter. Comece com as configurações básicas e explore gradualmente os recursos avançados conforme suas necessidades crescem.