How-To & Guides

TanStack Query: How to Eliminate Manual Server State Management in React

Learn how to use TanStack Query to manage cache, synchronization, and server states in React applications simply and efficiently.

TanStack Query: How to Eliminate Manual Server State Management in React

Learn how to use TanStack Query to manage data fetching, caching, updates, and synchronization in React applications, simplifying server state management.


Introduction to TanStack Query

Tired of manually managing loading states, handling network errors, and implementing complex caching logic in every component? TanStack Query, formerly known as React Query, eliminates this operational burden by automating data fetching, synchronization, and updates, allowing you to focus entirely on your React application logic.

Analogy: Imagine your React app is a restaurant. TanStack Query is like the kitchen manager who:

  • Knows which dishes are available (cache)

  • Checks if ingredients are fresh (staleTime)

  • Requests new ingredients when necessary (refetch)

  • Handles special orders (mutations)

  • Notifies when something is sold out (error)

Fundamental Concept 1: Basic Setup

First, we need to set up the QueryClient and wrap our application:

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>);

Explanation:

  • QueryClient manages cache and operations

  • QueryClientProvider provides the client to all components

  • defaultOptions defines global configurations

Tip: Keep a single instance of QueryClient for the entire application.

Fundamental Concept 2: Fetching Data with useQuery

The hook useQuery is used to fetch and synchronize data:

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>  );};

Explanation:

  • queryKey: Unique identifier for this query (organizes the cache)

  • queryFn: Asynchronous function that returns data or throws an error

  • isLoading: Indicates if the query is loading for the first time

  • isError: Indicates if an error occurred

  • error: Error object if isError is true

  • data: The data returned by the query

Expected result:

  • Initially displays "Loading..."

  • After fetching, displays a list of users or an error message

Fundamental Concept 3: Updating Data with useMutation

useMutation is used to create, update, or delete data:

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>  );};

Explanation:

  • useQueryClient: Accesses the client to invalidate queries

  • mutationFn: Function that executes the mutation

  • onSuccess: Callback executed when the mutation is successful

  • mutate: Function to trigger the mutation

  • isLoading: Indicates if the mutation is in progress

Advanced Concept: Query Configurations

TanStack Query offers many configurations to control behavior:

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,});

Practical Example 1: Pagination

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>  );};

Practical Example 2: Infinite Loading

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>  );};

Common Errors and How to Avoid Them

  1. Not invalidating queries after mutations

    • Problem: Displayed data does not reflect recent changes

    • Solution: Use onSuccess in useMutation to invalidate relevant queries

  2. Inconsistent query keys

    • Problem: The same endpoint with different parameters is treated as the same query

    • Solution: Include all relevant parameters in the queryKey

  3. Forgetting to handle error states

    • Problem: The application crashes when a network failure occurs

    • Solution: Always check isError and error in your components

  4. Using staleTime too short

    • Problem: Too many unnecessary requests to the server

    • Solution: Adjust the staleTime based on data update frequency

Best Practices

  • Use specific and predictable query keys

  • Keep fetch functions pure and without side effects

  • Handle all possible states (loading, error, success)

  • Configure staleTime and cacheTime appropriately for your use case

  • Use select to transform data in the component when necessary

  • Prefer invalidating queries over manual refetching

  • Document your query keys to facilitate maintenance

Going further

Takeaway: TanStack Query drastically simplifies server state management in React. By understanding the fundamental concepts of queries, mutations, and caching, you can build more responsive, resilient, and maintainable applications. Start with basic configurations and gradually explore advanced features as your needs grow.