blense.
HomeFrontendCodeAINewsGuidesCheatsheetsAbout

Technology decoded, always.

A curated read on AI, product, and the culture behind the code. No noise, no hype.

blense.

Technology with focus. Stories about what innovation really changes.

Sections

FrontendArtificial IntelligenceCode & DevTech News

Blense

AboutRSSPrivacy policyTermsCookies
© 2026 Blense · blense.fun
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.

Gby Genildo SouzaJul 86 min read
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.


What we will learn
  • 1- What TanStack Query is and why use it
  • 2- Basic QueryClient setup
  • 3- Data fetching with `useQuery`
  • 4- State management (loading, error, success)
  • 5- Data updates with `useMutation`
  • 6- Caching and query invalidation
  • 7- Advanced configurations (staleTime, cacheTime)
  • 8- Pagination and infinite loading
  • 9- Best practices and recommended patterns

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:

Analogia do TanStack Query como gerente de cozinha Usa Gerencia Verifica Solicita Aplicativo React O restaurante GERENTE TanStack Query Gerente de cozinha GERENTE Cache (pratos disponíveis) GERENTE staleTime (ingredientes frescos) GERENTE Refetch (solicita novos ingredientes)
TanStack Query analogy as a kitchen manager
  • 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

⚠️
Common Error: Not Invalidating Queries After Mutations

When you perform a mutation, the displayed data may become stale if related queries are not invalidated.

This causes UI inconsistency, showing information that does not reflect recent changes.

The solution is to use the onSuccess callback of useMutation to invalidate relevant queries, ensuring data is updated automatically.

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

The elite dev's arsenal.
Linux without rote learning: master the terminal with logic7 minMicrofrontends with Angular and Nx: what nobody tells you15 minAstro: Island Architecture, Performance, and Where the Framework Truly Fits38 minAngular Micro-frontends: How to Scale Large Applications and Eliminate Monolithic Build Times with Module Federation10 min

Going further

  • Official TanStack Query documentation

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.

Keep exploring
Want more content like this?

Check out other articles in the same vein and keep the momentum.

See more articles
#JavaScript#TypeScript

The elite dev's arsenal.

Linux without rote learning: master the terminal with logic
How-To & Guides

Linux without rote learning: master the terminal with logic

Master the Linux terminal by understanding its logical anatomy. From kernel to prompt, learn the core principles that make command line mastery easy.

Genildo Souza · Jun 18 · 7 min
In this article
  • Introduction to TanStack Query
  • Fundamental Concept 1: Basic Setup
  • Fundamental Concept 2: Fetching Data with useQuery
  • Fundamental Concept 3: Updating Data with useMutation
  • Advanced Concept: Query Configurations
  • Practical Example 1: Pagination
  • Practical Example 2: Infinite Loading
  • Common Errors and How to Avoid Them
  • Best Practices
  • Going further