Se você já construiu uma aplicação React com alguma complexidade, provavelmente escreveu o mesmo boilerplate dezenas de vezes: um useEffect para buscar dados, useState para loading e error, invalidação manual de cache quando algo muda. O React Query substitui tudo isso com uma API pequena e bem projetada que lida com cache, refetching em segundo plano e gerenciamento de dados desatualizados out of the box.

O que é React Query?

React Query é uma biblioteca para buscar, armazenar em cache e atualizar o estado do servidor em aplicações React. Ela não substitui Redux ou Zustand para estado client-side; ela lida com uma categoria completamente diferente de estado. O estado do servidor reside em um sistema remoto, pode ficar desatualizado e precisa de sincronização. O React Query foi construído especificamente para esse problema.

Ao lidar com cache e atualizações em segundo plano internamente, ele reduz a quantidade de estado que você gerencia manualmente e torna seus componentes mais fáceis de entender.

Por que usar React Query?

Gerenciar o data fetching manualmente significa rastrear no mínimo três partes do estado por requisição: loading, error e data. Adicione paginação, refetch ao focar na janela, deduplicação de requisições concorrentes e lógica de retry — e você terá centenas de linhas de infraestrutura para um problema que não é o seu produto. O React Query resolve tudo isso no nível da biblioteca.

Vantagens específicas:

  • Quando múltiplos componentes solicitam os mesmos dados simultaneamente, o React Query deduplica a requisição de rede e compartilha o resultado.
  • Os dados são buscados automaticamente em segundo plano quando ficam desatualizados, então os usuários veem conteúdo atualizado sem um reload manual.
  • Atualizações otimistas e invalidação de cache mantêm a UI consistente após mutações sem código de sincronização complexo.

Começando com React Query

Instalação:

npm install @tanstack/react-query

Antes de usar os hooks do React Query, configure um QueryClient e envolva sua aplicação com um QueryClientProvider:

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

const queryClient = new QueryClient();

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      {/* ... seus componentes */}
    </QueryClientProvider>
  );
}

export default App;

Entendendo Queries e Mutations

O React Query fornece dois hooks principais: useQuery para buscar dados e useMutation para criar, atualizar ou deletar dados.

O Hook useQuery:

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

function Todos() {
  const { isLoading, error, data } = useQuery(['todos'], fetchTodos);

  if (isLoading) return 'Loading...';

  if (error) return 'An error occurred';

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

O Hook useMutation:

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

function AddTodo() {
  const queryClient = useQueryClient();
  const mutation = useMutation(addTodo, {
    onSuccess: () => {
      // Invalidate and refetch
      queryClient.invalidateQueries(['todos']);
    },
  });

  return (
    <button
      onClick={() => {
        mutation.mutate({ title: 'New Todo' });
      }}
    >
      Add Todo
    </button>
  );
}

Configurando um Caso de Uso Completo

Aqui está uma aplicação CRUD básica para gerenciar uma lista de usuários, usando a API JSONPlaceholder.

A aplicação lida com: exibir uma lista de usuários, adicionar um novo usuário, atualizar um usuário existente e deletar um usuário.

Configurando o Projeto:

npx create-react-app react-query-demo
cd react-query-demo
npm install @tanstack/react-query axios

Usaremos Axios para fazer as requisições HTTP.

Implementando React Query na Aplicação:

  1. Configurar QueryClient
// index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import App from './App';

const queryClient = new QueryClient();

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <QueryClientProvider client={queryClient}>
    <App />
  </QueryClientProvider>
);
  1. Buscar Usuários
// App.js
import React from 'react';
import axios from 'axios';
import { useQuery } from '@tanstack/react-query';

function App() {
  const { isLoading, error, data } = useQuery(['users'], () =>
    axios.get('https://jsonplaceholder.typicode.com/users').then(res => res.data)
  );

  if (isLoading) return <p>Loading users...</p>;

  if (error) return <p>An error occurred: {error.message}</p>;

  return (
    <div>
      <h1>User List</h1>
      <ul>
        {data.map(user => (
          <li key={user.id}>
            {user.name}
            {/* Update and Delete buttons will go here */}
          </li>
        ))}
      </ul>
      {/* Components for adding a user will go here */}
    </div>
  );
}

export default App;
  1. Adicionar um Usuário

Como o JSONPlaceholder não altera dados de verdade, vamos simular a adição de um usuário.

// AddUser.js
import React, { useState } from 'react';
import axios from 'axios';
import { useMutation, useQueryClient } from '@tanstack/react-query';

function AddUser() {
  const queryClient = useQueryClient();
  const [name, setName] = useState('');

  const addUserMutation = useMutation(
    newUser => axios.post('https://jsonplaceholder.typicode.com/users', newUser).then(res => res.data),
    {
      onSuccess: data => {
        // Update the users list
        queryClient.setQueryData(['users'], oldData => [...oldData, data]);
      },
    }
  );

  const handleSubmit = e => {
    e.preventDefault();
    addUserMutation.mutate({ name });
    setName('');
  };

  return (
    <form onSubmit={handleSubmit}>
      <input value={name} onChange={e => setName(e.target.value)} placeholder="Name" required />
      <button type="submit">Add User</button>
    </form>
  );
}

export default AddUser;

Inclua <AddUser /> no seu componente App.

// App.js
// ...
import AddUser from './AddUser';
// ...
function App() {
  // existing code
  return (
    <div>
      {/* existing code */}
      <AddUser />
    </div>
  );
}
// ...
  1. Atualizar um Usuário
// UpdateUser.js
import React, { useState } from 'react';
import axios from 'axios';
import { useMutation, useQueryClient } from '@tanstack/react-query';

function UpdateUser({ user }) {
  const queryClient = useQueryClient();
  const [name, setName] = useState(user.name);

  const updateUserMutation = useMutation(
    updatedUser => axios.put(`https://jsonplaceholder.typicode.com/users/${user.id}`, updatedUser).then(res => res.data),
    {
      onSuccess: data => {
        queryClient.setQueryData(['users'], oldData =>
          oldData.map(u => (u.id === user.id ? data : u))
        );
      },
    }
  );

  const handleSubmit = e => {
    e.preventDefault();
    updateUserMutation.mutate({ name });
  };

  return (
    <form onSubmit={handleSubmit}>
      <input value={name} onChange={e => setName(e.target.value)} required />
      <button type="submit">Update</button>
    </form>
  );
}

export default UpdateUser;

Inclua <UpdateUser user={user} /> na renderização da sua lista de usuários.

// App.js
// ...
<li key={user.id}>
  {user.name}
  <UpdateUser user={user} />
  {/* Delete button */}
</li>
// ...
  1. Deletar um Usuário
// DeleteUser.js
import React from 'react';
import axios from 'axios';
import { useMutation, useQueryClient } from '@tanstack/react-query';

function DeleteUser({ userId }) {
  const queryClient = useQueryClient();

  const deleteUserMutation = useMutation(
    () => axios.delete(`https://jsonplaceholder.typicode.com/users/${userId}`),
    {
      onSuccess: () => {
        queryClient.setQueryData(['users'], oldData =>
          oldData.filter(user => user.id !== userId)
        );
      },
    }
  );

  return <button onClick={() => deleteUserMutation.mutate()}>Delete</button>;
}

export default DeleteUser;

Inclua <DeleteUser userId={user.id} /> na renderização da sua lista de usuários.

// App.js
// ...
<li key={user.id}>
  {user.name}
  <UpdateUser user={user} />
  <DeleteUser userId={user.id} />
</li>
// ...

Funcionalidades Avançadas

Lidando com Estados de Query:

O React Query fornece vários estados para gerenciar cenários de loading, error e sucesso de forma eficaz.

const {
  isLoading,
  isError,
  data,
  error,
  isFetching,
  refetch,
} = useQuery(['todos'], fetchTodos);
  • isLoading: true no primeiro carregamento antes de os dados chegarem.
  • isError: true se a query encontrar um erro.
  • data: o resultado buscado.
  • error: o objeto de erro quando a query falha.
  • isFetching: permanece true durante refetches em segundo plano mesmo quando os dados já estão em cache.
  • refetch: chame isso para disparar um refetch manualmente.

Estratégias de Cache:

useQuery(['users'], fetchUsers, {
  cacheTime: 1000 * 60 * 10, // Cache data for 10 minutes
  staleTime: 1000 * 60 * 5,  // Data is fresh for 5 minutes
});

Polling ou Busca por Intervalo:

useQuery(['notifications'], fetchNotifications, {
  refetchInterval: 1000 * 60, // Refetch every 60 seconds
});

Retentando Queries com Falha:

useQuery(['users'], fetchUsers, {
  retry: 3, // Retry up to 3 times
  retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30000),
});

React Query Devtools:

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

function App() {
  return (
    // ...
    <ReactQueryDevtools initialIsOpen={false} />
  );
}

Server-Side Rendering:

// pages/_app.js
// Implement getServerSideProps or getStaticProps

Funções de Query Customizadas:

const fetcher = url => axios.get(url).then(res => res.data);

useQuery(['users'], () => fetcher('/api/users'));

Transformando Dados:

useQuery(['users'], fetchUsers, {
  select: data => data.map(user => user.name),
});

Boas Práticas

  • Use query keys descritivas e estruturadas: ['users', userId] é mais fácil de invalidar seletivamente do que 'user'.
  • Trate erros com callbacks onError e os exiba explicitamente para o usuário.
  • Use paginação ou scroll infinito para grandes conjuntos de dados para evitar carregar tudo de uma vez.
  • Ajuste o staleTime por query. Dados que raramente mudam podem ter um staleTime alto; dados em tempo real devem ter zero.

Alternativas ao React Query

SWR, desenvolvido pela Vercel, cobre território similar com uma API mais simples, mas com menos funcionalidades avançadas. O Apollo Client vale a pena considerar se você já usa GraphQL e quer normalização de cache integrada.

Conclusão

O React Query remove uma categoria substancial de boilerplate das aplicações React. Após integrá-lo, você perceberá que a maior parte da sua lógica de data fetching com useEffect desaparece, e os componentes que restam são mais fáceis de testar e entender. A curva de aprendizado está no sistema de query keys e em entender quando invalidar versus atualizar otimisticamente, mas ambos se tornam intuitivos rapidamente.

Leitura Adicional