Gerenciando Estado Remoto no React com TanStack Query

Série: Arquitetura Moderna para CRUDs no Next.js
- ✅ Parte 1 — Arquitetando um CRUD Escalável
- ✅ Parte 2 — Construindo uma Camada Profissional de Comunicação
- 👉 Parte 3 — Gerenciando Estado Remoto com TanStack Query (Você está aqui)
- ⏳ Parte 4 — Formulários Escaláveis com React Hook Form + Zod
- ⏳ Parte 5 — Search Params, Filtros e Paginação
- ⏳ Parte 6 — UX Moderna: Optimistic Updates e Performance
Estado Remoto: o problema que ainda não resolvemos
Nos artigos anteriores construímos uma arquitetura sólida para comunicação com a API.
Hoje nossa aplicação possui uma separação clara de responsabilidades:
Componentes
↓
Server Actions
↓
Services
↓
Axios
↓
API
Tudo está organizado.
Cada camada possui uma única responsabilidade e o código ficou muito mais fácil de manter.
Entretanto, existe um problema que ainda não resolvemos.
O mesmo dado sendo buscado várias vezes
Imagine uma página com três componentes diferentes.
A listagem principal precisa buscar usuários.
const users = await getUsersService();
Um painel lateral também precisa dessa lista.
const users = await getUsersService();
E um modal de seleção faz exatamente a mesma chamada.
const users = await getUsersService();
Quantas requisições HTTP foram feitas?
Resposta: três.
Mesmo endpoint.
Mesmo retorno.
Mesmo banco de dados.
Mesmo usuário.
Cada componente dispara sua própria requisição.
Isso aumenta o tempo de carregamento, desperdiça banda e sobrecarrega o servidor sem necessidade.
O problema fica ainda maior
Agora imagine outro cenário.
O usuário altera o nome de João para João Silva e salva.
Quem atualiza os componentes?
- A listagem?
- O painel lateral?
- O modal?
Se você não fizer absolutamente nada, todos continuarão exibindo o nome antigo até que o usuário atualize a página.
Perceba que o problema deixou de ser buscar dados.
Nós já resolvemos isso utilizando Services.
Agora o desafio é outro:
Como administrar os dados vindos do servidor?
React nunca foi feito para resolver isso
Quando esse problema aparece, muita gente tenta resolvê-lo utilizando apenas as ferramentas nativas do React:
useStateuseEffectContext API
Essas ferramentas são excelentes...
...para estado local.
const [isModalOpen, setIsModalOpen] = useState(false);
Esse estado pertence exclusivamente ao componente.
Quando o componente desmonta, ele desaparece.
O React sabe exatamente quando ele muda.
Mas isto não é estado local
const users = await getUsersService();
A lista de usuários não pertence ao React.
Ela pertence ao servidor.
Ela pode mudar a qualquer momento.
Esse tipo de dado recebe um nome:
Estado Remoto (Server State).
Estado remoto possui características diferentes
O estado remoto possui propriedades que tornam seu gerenciamento muito mais complexo.
- É assíncrono.
- Pode demorar para chegar.
- Pode estar desatualizado.
- Pode ser alterado por outro usuário.
- Precisa de cache.
- Precisa ser sincronizado.
- Precisa ser invalidado após mutações.
O React nunca tentou resolver esses problemas.
E nem deveria.
Foi exatamente para isso que nasceu o TanStack Query.
A nova peça da arquitetura
Nossa arquitetura praticamente não muda.
Ela apenas ganha uma nova camada.
Antes
Componentes
↓
Server Actions
↓
Services
↓
Axios
↓
API
Depois
Componentes
↓
TanStack Query
↓
Server Actions
↓
Services
↓
Axios
↓
API
Observe que:
- continuamos usando Services;
- continuamos usando Axios;
- continuamos usando Server Actions.
A única diferença é que agora existe uma camada responsável por administrar os dados vindos da API.
Configurando o motor
A configuração é extremamente simples.
Criamos um QueryClient e envolvemos a aplicação com um QueryClientProvider.
Esse objeto será responsável por manter um cache compartilhado dentro do navegador.
Em outras palavras:
O QueryClient funciona como um pequeno banco de dados em memória.
O primeiro useQuery
Agora podemos abandonar completamente o padrão useEffect + useState.
"use client";
import { useQuery } from "@tanstack/react-query";
import { getUsersAction } from "@/actions/users/get-users-action";
export function UsersList() {
const { data, isLoading, isError } = useQuery({
queryKey: ["users"],
queryFn: () => getUsersAction(),
});
if (isLoading) return <p>Carregando...</p>;
if (isError) return <p>Falha ao carregar usuários.</p>;
return (
<ul>
{data?.items.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
O que o TanStack Query faz?
Na prática, ele executa apenas três responsabilidades.
1. Buscar
Executa a função responsável pela requisição.
queryFn
2. Guardar
Armazena o resultado em cache.
3. Sincronizar
Notifica todos os componentes que dependem daquele dado.
Assim que o cache muda, toda a interface é atualizada automaticamente.
Sem eventos.
Sem Context API.
Sem Redux.
Sem dezenas de estados espalhados pela aplicação.
A chave de tudo: Query Keys
Na minha opinião, este é o conceito mais importante de toda a biblioteca.
queryKey: ["users"]
A Query Key funciona como o RG da requisição.
Ela identifica exatamente qual dado está armazenado no cache.
["users"]
Lista completa.
["users", 1]
Usuário de ID 1.
["users", { status: "active", page: 2 }]
Página 2 dos usuários ativos.
Sempre que qualquer valor da chave muda, o TanStack Query entende que se trata de outro recurso e cria um novo cache automaticamente.
Esse conceito será essencial quando trabalharmos com filtros, paginação e busca.
Agora a mágica acontece
Vamos voltar ao problema inicial.
Temos três componentes.
Todos pedem:
["users"]
O fluxo passa a ser o seguinte:
- O primeiro componente faz a requisição.
- O TanStack Query salva o resultado.
- O segundo componente pede os mesmos dados.
- O cache responde instantaneamente.
- Nenhuma nova requisição HTTP é realizada.
Todos os componentes compartilham exatamente o mesmo estado remoto.
staleTime
Outra funcionalidade extremamente importante é o staleTime.
staleTime: 1000 * 60 * 5
Nesse exemplo estamos dizendo:
Considere estes dados válidos durante cinco minutos.
Durante esse período:
- não existem novas requisições;
- a navegação fica instantânea;
- o usuário praticamente não vê loading;
- o servidor recebe muito menos chamadas.
É uma melhoria enorme de experiência e performance.
Conclusão
Curiosamente, nossos Services não precisaram mudar uma única linha.
Isso só foi possível porque a arquitetura foi construída respeitando responsabilidades bem definidas.
Cada camada continua fazendo exatamente aquilo que deveria fazer.
- Os Services conversam com a API.
- O Axios faz a comunicação HTTP.
- O TanStack Query administra o estado remoto.
- Os Componentes apenas exibem os dados.
Essa é a principal característica de uma arquitetura escalável:
Ela cresce adicionando novas camadas, e não reescrevendo as antigas.
No próximo artigo vamos resolver outro problema fundamental.
Buscar dados ficou extremamente simples.
Agora chegou a hora de enviá-los de volta ao servidor de forma segura utilizando React Hook Form, Zod e Server Actions.