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.

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:
// 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:
QueryClientmanages cache and operationsQueryClientProviderprovides the client to all componentsdefaultOptionsdefines 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:
// 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 errorisLoading: Indicates if the query is loading for the first timeisError: Indicates if an error occurrederror: Error object ifisErroris truedata: 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:
// 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 queriesmutationFn: Function that executes the mutationonSuccess: Callback executed when the mutation is successfulmutate: Function to trigger the mutationisLoading: Indicates if the mutation is in progress
Advanced Concept: Query Configurations
TanStack Query offers many configurations to control behavior:
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
// 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
// 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
Not invalidating queries after mutations
Problem: Displayed data does not reflect recent changes
Solution: Use
onSuccessinuseMutationto invalidate relevant queries
Inconsistent query keys
Problem: The same endpoint with different parameters is treated as the same query
Solution: Include all relevant parameters in the
queryKey
Forgetting to handle error states
Problem: The application crashes when a network failure occurs
Solution: Always check
isErroranderrorin your components
Using
staleTimetoo shortProblem: Too many unnecessary requests to the server
Solution: Adjust the
staleTimebased 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
staleTimeandcacheTimeappropriately for your use caseUse
selectto transform data in the component when necessaryPrefer 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.
