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.
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)
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:
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.
useQueryThe 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 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
useMutationuseMutation 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 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
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,});// 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> );};// 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> );};Not invalidating queries after mutations
Problem: Displayed data does not reflect recent changes
Solution: Use onSuccess in useMutation to 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 isError and error in your components
Using staleTime too short
Problem: Too many unnecessary requests to the server
Solution: Adjust the staleTime based on data update frequency
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
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.