Percio Castelo.
EN Início
TanStack Query vs. useEffect: O Guia Definitivo de Data Fetching, Governança de Estado e Performance no React
← Voltar para blog

TanStack Query vs. useEffect: O Guia Definitivo de Data Fetching, Governança de Estado e Performance no React

14/06/2026 · 11 min · Desenvolvimento · 810 visualizações

English Edition Available: Read the full English version of this article: TanStack Query vs. useEffect: The Definitive Guide to Data Fetching, State Governance, and React Performance.

1. O Problema Real do useEffect para Fetch e a Mudança de Camada para Estado Remoto#

O useEffect não foi concebido para busca de dados; sua função original é sincronizar o componente com efeitos colaterais externos (como subscrições ou manipulação manual do DOM). Ao usá-lo para requisições assíncronas, é comum deparar-se com problemas crônicos de arquitetura:

  • Race Conditions Silenciosas: Se o usuário alterna rapidamente entre páginas ou altera um filtro antes que a requisição anterior termine, múltiplos callbacks de resolução serão disparados. O componente exibirá os dados da requisição que terminar por último na rede, e não da mais recente iniciada pelo usuário.
  • Ausência de Deduplicação de Requisições: Se três componentes na mesma tela precisam dos dados de usuários, o useEffect fará três requisições HTTP paralelas.
  • Vazamento de Recursos (Memory Leaks): Em React 17 e anteriores, tentar atualizar o estado de um componente desmontado gerava o warning: Can't perform a React state update on an unmounted component. No React 18+, esse aviso foi removido do console, mas o vazamento de recursos continua: a requisição HTTP continua rodando no navegador e a CPU processa a resposta em segundo plano de forma desnecessária.
  • Ausência de Cache: A interface pisca e mostra um loading toda vez que o componente é remontado, prejudicando a experiência de uso.

Comparativo Head-to-Head: useEffect vs. TanStack Query#

Ao comparar o padrão manual com useEffect e a abstração declarativa do TanStack Query, a diferença prática em complexidade de código e governança de estado fica evidente:

Recurso / CritériouseEffect + useState ManualTanStack Query (React Query)
Volume de BoilerplateAlto (requer 3 estados manuais: data, loading, error)Mínimo (1 chamada hook com retorno estruturado)
Cache em MemóriaInexistente (fetch repetido a cada remontagem)Nativo e automático com controle via staleTime e gcTime
Deduplicação de RequestsNão (requisições simultâneas duplicam chamadas de rede)Sim (agrupa e compartilha a mesma promise por Query Key)
Race ConditionsFrequentes em filtros/inputs rápidos (requer AbortController)Resolvidas por design (chaves dinâmicas cancelam/ignoram queries antigas)
Revalidação em BackgroundManual via listeners de foco de janela ou timersAutomática ao refocar aba (refetchOnWindowFocus) ou reconectar
Gerenciamento de MutaçõesEfeitos colaterais manuais e refetch manual imperativouseMutation com invalidação declarativa (invalidateQueries)
Rollback e Atualização OtimistaExtremamente complexo de sincronizar e reverterSuporte de primeira classe com snapshots de contexto no rollback
Paginação e Infinite ScrollExige arquitetura complexa de junção de arraysHooks dedicados (useInfiniteQuery) com cursores prontos
Tratamento de Falhas (Retries)Requer loops de retry manuais com backoffRetries exponenciais automáticos configuráveis
Vazamentos de Memória (Leaks)Alto risco ao desmontar componentes durante fetchLimpeza automática gerenciada pelo ciclo de vida da query

A Mudança de Camada: Sincronização de Estado Remoto#

Com React Query, a mentalidade muda de "buscar dados" para "sincronizar dados". O componente deixa de carregar a responsabilidade de gerenciar políticas de cache e consistência. Essa política sobe para uma camada dedicada de infraestrutura.

Ciclo de vida do cache#

O TanStack Query gerencia o dado em quatro estados sequenciais:

flowchart LR FRESH["🟢 Fresh\n(staleTime não expirou)"] STALE["🟡 Stale\n(pronto para refetch)"] INACTIVE["⚪ Inactive\n(nenhum subscriber)"] GC["🗑️ Garbage Collected\n(removido do cache)"] FRESH -->|"staleTime expira"| STALE STALE -->|"componente desmonta"| INACTIVE STALE -->|"janela foca /\ninterval dispara"| FRESH INACTIVE -->|"gcTime expira"| GC INACTIVE -->|"componente monta novamente"| STALE style FRESH fill:#14532d,color:#86efac style STALE fill:#78350f,color:#fde68a style INACTIVE fill:#374151,color:#d1d5db style GC fill:#7f1d1d,color:#fca5a5
  • Fresh: dado recente, não será revalidado automaticamente.
  • Stale: dado considerado desatualizado; revalida em background no próximo trigger (foco de janela, interval, remount).
  • Inactive: nenhum componente está inscrito nessa query key; o dado permanece no cache até gcTime expirar.
  • Garbage Collected: o dado foi removido da memória. O próximo acesso fará um fetch do zero.

Usequery: padrões essenciais de busca e sincronização#

Abaixo está uma implementação típica utilizando tipagem TypeScript, validação de payload em tempo de execução com Zod para evitar estados corrompidos, e parâmetros de cacheamento dinâmico:

import { useQuery } from "@tanstack/react-query";
import { z } from "zod";

// 1. Definição do Schema de validação
const UserSchema = z.object({
  id: z.number(),
  name: z.string(),
  email: z.string().email(),
});
const UsersArraySchema = z.array(UserSchema);

type User = z.infer<typeof UserSchema>;

// 2. Fetcher tipado e validado
const fetchUsers = async (status?: string): Promise<User[]> => {
  const url = status ? `/api/users?status=${status}` : "/api/users";
  const res = await fetch(url);
  if (!res.ok) throw new Error("Erro na comunicação com a API");
  const data = await res.json();
  return UsersArraySchema.parse(data); // Validação estrita
};

export default function Users({ status }: { status?: string }) {
  const { data, isLoading, isFetching, error } = useQuery<User[], Error>({
    queryKey: ["users", status],
    queryFn: () => fetchUsers(status),
    
    // Configurações de Ciclo de Vida do Cache
    staleTime: 1000 * 60 * 5,       // Dado é considerado fresco por 5 minutos
    gcTime: 1000 * 60 * 30,          // Mantém dados em cache inativo por 30 minutos
    retry: 2,                        // Retenta até 2 vezes antes de falhar
    
    // Busca condicional: Só executa se o status estiver definido
    enabled: !!status,
    // Nota: `null`, `undefined`, `0` e `""` também são falsy e desabilitam a query.
    // Use `enabled: status !== undefined` para permitir strings vazias como busca válida.
    
    // Polling ativo (revalidação em background a cada 30 segundos)
    refetchInterval: 30000,
    refetchIntervalInBackground: false, // Pausa polling ao perder foco da aba
    refetchOnWindowFocus: true,         // Revalida ao focar na janela do app
  });

  if (isLoading) return <p>Carregando dados (primeira busca)...</p>;
  if (error) return <p>Erro: {error.message}</p>;

  return (
    <div>
      {isFetching && <span>Atualizando dados em segundo plano...</span>}
      <ul>
        {data?.map(user => (
          <li key={user.id}>{user.name} ({user.email})</li>
        ))}
      </ul>
    </div>
  );
}

Dependências dinâmicas no queryKey#

O queryKey funciona como o array de dependências do useEffect: sempre que um valor dentro dele mudar, o TanStack Query dispara automaticamente um novo fetch. Isso é o que torna o exemplo ["users", status] tão poderoso:

// Sempre que `status` mudar (ex: "active" → "inactive"),
// a query reexecuta automaticamente com o novo parâmetro
const { data } = useQuery({
  queryKey: ["users", status],    // status entra como parte da chave
  queryFn: () => fetchUsers(status),
});

// Analogia com useEffect:
useEffect(() => {
  fetchUsers(status);
}, [status]); // mesmo comportamento, mas sem cache, deduplication ou retry

Sempre que precisar que um dado refetch ao alterar filtros, páginas ou parâmetros de busca, inclua esses valores no queryKey. O TanStack Query garante que cada combinação de chave mantenha seu próprio cache isolado.

UX fluida em paginações#

Para evitar transições bruscas de tela durante paginações ou buscas dinâmicas, o parâmetro placeholderData permite manter o estado anterior enquanto novos dados são computados:

import { keepPreviousData } from "@tanstack/react-query";

const { data } = useQuery({
  queryKey: ["users", page],
  queryFn: () => fetchUsersPage(page),
  placeholderData: keepPreviousData, // Evita layout shifts e spinners de loading
});

2. A Solução: Migração Prática de Código (Antes vs. Depois)#

Antes: o boilerplate excessivo com useEffect#

Para resolver race conditions (usando um sinalizador de cancelamento) e controlar estados de loading/error manualmente:

import { useState, useEffect } from 'react';

function ListaUsuarios() {
  const [usuarios, setUsuarios] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    let active = true;
    setLoading(true);

    fetch('/api/usuarios')
      .then((res) => {
        if (!res.ok) throw new Error('Falha ao obter usuários');
        return res.json();
      })
      .then((data) => {
        if (active) {
          setUsuarios(data);
          setLoading(false);
        }
      })
      .catch((err) => {
        if (active) {
          setError(err);
          setLoading(false);
        }
      });

    return () => {
      active = false;
    };
  }, []);

  if (loading) return <Skeleton />;
  if (error) return <ErroComponente mensagem={error.message} />;
  return <TabelaUsuarios dados={usuarios} />;
}

Depois: declarativo com tanstack query (v5)#

import { useQuery } from '@tanstack/react-query';

function ListaUsuarios() {
  const { data: usuarios, isLoading, error } = useQuery({
    queryKey: ['usuarios'],
    queryFn: () => fetch('/api/usuarios').then((r) => {
      if (!r.ok) throw new Error('Erro de rede');
      return r.json();
    }),
  });

  if (isLoading) return <Skeleton />;
  
  if (error) {
    const mensagem = error instanceof Error ? error.message : 'Erro desconhecido';
    return <ErroComponente mensagem={mensagem} />;
  }

  return <TabelaUsuarios dados={usuarios ?? []} />;
}

3. Tipagem com TypeScript e Tratamento Seguro de Erros#

No TanStack Query v5, o tipo padrão do objeto error é unknown por segurança, já que qualquer exceção pode ser lançada na queryFn. Acessar .message diretamente causa erros de compilação.

Tipagem forte dos dados e do erro#

É possível tipar a query passando parâmetros genéricos na chamada ou deixando a função resolver implicitamente:

interface Usuario {
  id: number;
  nome: string;
  email: string;
}

// O Hook infere o tipo retornado por queryFn como `Usuario[] | undefined`
const { data, isLoading, error } = useQuery<Usuario[], Error>({
  queryKey: ['usuarios'],
  queryFn: async () => {
    const res = await fetch('/api/usuarios');
    if (!res.ok) throw new Error('Falha na API');
    return res.json();
  },
  staleTime: 1000 * 60 * 5, // Dados válidos em cache por 5 minutos
});

if (error) {
  // TypeScript agora sabe que error é do tipo Error
  return <p>Ocorreu um erro: {error.message}</p>;
}

Uso com react error boundaries#

Se você prefere capturar erros usando um Error Boundary global do React, utilize a propriedade throwOnError:

const { data } = useQuery({
  queryKey: ['usuarios'],
  queryFn: fetchUsuarios,
  throwOnError: true, // Propaga o erro para o ancestral Error Boundary
});

4. Buscas Condicionais, Dependentes e Suporte a Suspense#

Muitas vezes, a busca de dados depende de uma variável (ex: um ID selecionado) ou deve acontecer apenas após uma ação do usuário. A flag enabled controla essa execução:

const { data: detalhes, isLoading } = useQuery({
  queryKey: ['usuario', usuarioId],
  queryFn: () => fetch(`/api/usuarios/${usuarioId}`).then(r => r.json()),
  enabled: !!usuarioId, // Só dispara o fetch se usuarioId for válido
});

Data Fetching Declarativo com Suspense#

O TanStack Query suporta integração nativa com <Suspense> do React para simplificar estados de carregamento. Em vez de checar isLoading em cada componente, você delega o estado de loading para um Suspense boundary mais acima na árvore:

import { useSuspenseQuery } from "@tanstack/react-query";
import { Suspense } from "react";

// Hook: use useSuspenseQuery (não useQuery) para modo Suspense
function UserList({ status }: { status: string }) {
  const { data } = useSuspenseQuery({
    queryKey: ["users", status],
    queryFn: () => fetchUsers(status),
  });
  // data aqui é sempre definido (nunca undefined)
  return <ul>{data.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}

// Pai: gerencia o estado de loading via boundary
export function UsersPage({ status }: { status: string }) {
  return (
    <Suspense fallback={<p>Carregando usuários...</p>}>
      <UserList status={status} />
    </Suspense>
  );
}

5. Operações de Escrita: Mutações com useMutation e Invalidação de Cache#

Buscar dados (queries) é apenas metade do ciclo CRUD. Para operações de escrita (POST, PUT, DELETE), o TanStack Query fornece o hook useMutation, integrado com invalidação de cache para manter a interface atualizada.

import { useMutation, useQueryClient } from '@tanstack/react-query';

interface NovoUsuario {
  nome: string;
  email: string;
}

function CriarUsuario() {
  const queryClient = useQueryClient();

  const mutation = useMutation({
    mutationFn: async (novoUsuario: NovoUsuario) => {
      const res = await fetch('/api/usuarios', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(novoUsuario),
      });
      if (!res.ok) throw new Error('Erro ao criar usuário');
      return res.json();
    },
    onSuccess: () => {
      // Invalida o cache e força todos os componentes inscritos em 'usuarios' a rebuscar os dados
      queryClient.invalidateQueries({ queryKey: ['usuarios'] });
    },
  });

  return (
    <div>
      {mutation.isError && <p>Erro: {mutation.error.message}</p>}
      
      <button
        onClick={() => mutation.mutate({ nome: 'Percio', email: '[email protected]' })}
        disabled={mutation.isPending}
      >
        {mutation.isPending ? 'Salvando...' : 'Adicionar Usuário'}
      </button>
    </div>
  );
}

Controle Avançado de Mutações e Invalidação#

Para fluxos de alteração de dados (POST, PUT, DELETE), o React Query fornece o hook useMutation. Ao contrário de queries, mutações não disparam requisições de forma implícita e são essenciais para gerenciar side-effects, revalidações e rollbacks.

Invalidação simples de cache pós-sucesso#

import { useMutation, useQueryClient } from "@tanstack/react-query";

interface CreateUserInput {
  name: string;
  email: string;
}

export function CreateUserButton() {
  const queryClient = useQueryClient();

  const mutation = useMutation<User, Error, CreateUserInput>({
    mutationFn: async (newUser) => {
      const res = await fetch("/api/users", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(newUser),
      });
      if (!res.ok) throw new Error("Erro ao criar usuário");
      return res.json();
    },
    onSuccess: () => {
      // Invalida e força re-fetch imediato da lista de usuários para manter sync
      queryClient.invalidateQueries({ queryKey: ["users"] });
    },
  });

  return (
    <button 
      onClick={() => mutation.mutate({ name: "João", email: "[email protected]" })}
      disabled={mutation.isPending}
    >
      {mutation.isPending ? "Salvando..." : "Criar Usuário"}
    </button>
  );
}

Optimistic updates (atualização otimista) com rollback#

Em redes móveis ou de alta latência, é possível atualizar o cache local instantaneamente e fazer rollback caso a API falhe:

const mutation = useMutation({
  mutationFn: updateUserData,
  
  // Executa assim que a mutação é chamada
  onMutate: async (newData) => {
    // 1. Cancela refetches da lista para evitar sobrescrever o estado otimista
    await queryClient.cancelQueries({ queryKey: ["users"] });
    
    // 2. Salva o snapshot do cache atual (para rollback de emergência)
    const previousUsers = queryClient.getQueryData<User[]>(["users"]);
    
    // 3. Atualiza o cache de forma otimista
    queryClient.setQueryData<User[]>(["users"], (old) => 
      old ? old.map(u => u.id === newData.id ? { ...u, ...newData } : u) : []
    );
    
    // Retorna o snapshot salvo
    return { previousUsers };
  },
  
  // Se a mutação falhar, reverte para o estado anterior
  onError: (err, newData, context) => {
    if (context?.previousUsers) {
      queryClient.setQueryData(["users"], context.previousUsers);
    }
  },
  
  // Roda sempre após sucesso ou falha, garantindo o sync final
  onSettled: () => {
    queryClient.invalidateQueries({ queryKey: ["users"] });
  },
});

6. Configuração Global do QueryClient e DevTools#

Configurar opções padrão de forma global evita que você precise digitar políticas de cache em todas as consultas.

Configurando o client com políticas de cache#

import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 1000 * 60 * 5,       // Dados frescos por 5 minutos
      gcTime: 1000 * 60 * 30,          // Mantém no cache (Garbage Collector) por 30 minutos
      retry: 3,                        // Tenta 3 vezes antes de falhar
      refetchOnWindowFocus: true,      // Revalida automaticamente se focar na aba
      refetchOnReconnect: true,        // Revalida se a internet voltar
    },
  },
});

Habilitando o tanstack query devtools#

O DevTools oficial é essencial para depurar estados do cache, conferir chaves e depurar mutações em tempo de execução:

pnpm add @tanstack/react-query-devtools

Adicione o painel do DevTools logo abaixo do QueryClientProvider na raiz da sua aplicação:

import { ReactQueryDevtools } from '@tanstack/react-query-devtools';

ReactDOM.createRoot(document.getElementById('root')!).render(
  <QueryClientProvider client={queryClient}>
    <App />
    <ReactQueryDevtools initialIsOpen={false} />
  </QueryClientProvider>
);

7. Otimização de Performance, Governança de Cache (staleTime vs. gcTime) e UX#

A. Polling automático (real-time)#

Para painéis de monitoramento que necessitam de atualização periódica e contínua:

const { data } = useQuery({
  queryKey: ['dashboard-status'],
  queryFn: fetchStatus,
  refetchInterval: 10000,              // Executa a query a cada 10 segundos
  refetchIntervalInBackground: false,  // Desativa o polling quando a aba estiver em background
});

B. Paginação fluida com placeholderData#

Evite que a interface sofra layout shifts (piscadas com loading skeleton) ao trocar de página no navegador. O parâmetro placeholderData mantém os dados antigos visíveis enquanto carrega a nova página:

import { keepPreviousData } from '@tanstack/react-query';

function ListaPaginada({ pagina }: { pagina: number }) {
  const { data, isPlaceholderData } = useQuery({
    queryKey: ['usuarios', pagina],
    queryFn: () => fetch(`/api/usuarios?page=${pagina}`).then(r => r.json()),
    placeholderData: keepPreviousData, // UX perfeito: exibe página atual opaca enquanto busca a próxima
  });

  return (
    <div style={{ opacity: isPlaceholderData ? 0.6 : 1, transition: 'opacity 0.2s' }}>
      <TabelaUsuarios dados={data?.resultado ?? []} />
    </div>
  );
}

C. Transformação de dados com select#

Permite filtrar ou transformar a resposta da query antes de expô-la ao componente, otimizando renders (o componente só re-renderiza se os dados transformados mudarem):

const { data: totalAtivos } = useQuery({
  queryKey: ['usuarios'],
  queryFn: fetchUsuarios,
  select: (usuarios: Usuario[]) => usuarios.filter(u => u.status === 'ativo').length,
});

8. Segurança e Validação de Dados em Tempo de Execução com Zod#

Se a API retornar um payload com propriedades ausentes ou tipos trocados, seu código TypeScript pode falhar em runtime. Validar o payload no fetcher com Zod garante a integridade dos dados na entrada do app:

import { z } from 'zod';
import { useQuery } from '@tanstack/react-query';

const UsuarioSchema = z.object({
  id: z.number(),
  nome: z.string(),
  email: z.string().email(),
});

const ListaUsuariosSchema = z.array(UsuarioSchema);

function HookUsuariosValidados() {
  return useQuery({
    queryKey: ['usuarios-validados'],
    queryFn: async () => {
      const res = await fetch('/api/usuarios');
      const data = await res.json();
      // O parse lança um erro detalhado se a API quebrar o contrato
      return ListaUsuariosSchema.parse(data); 
    },
  });
}

9. Matriz de Decisão: Quando Ainda Usar useEffect e Alternativas no Ecossistema#

1. Centralização de chaves (query key factories)#

Evite o uso de strings soltas espalhadas pelo projeto para evitar erros de digitação ao invalidar o cache:

export const queryKeys = {
  usuarios: {
    all: ['usuarios'] as const,
    list: (filtro: string) => [...queryKeys.usuarios.all, 'lista', filtro] as const,
    detail: (id: number) => [...queryKeys.usuarios.all, 'detalhe', id] as const,
  },
};

// Uso:
useQuery({ queryKey: queryKeys.usuarios.detail(1), queryFn: ... })

2. Comparações e alternativas#

Embora o TanStack Query seja o ecossistema mais adotado, é bom conhecer as alternativas para cada cenário de projeto:

BibliotecaPeso (Gzipped)Casos de Uso RecomendadosPrincipais Diferenciais
TanStack Query~13kBAplicações SaaS complexas, dashboards robustosGestão de cache granular, mutações, excelente ecossistema e devtools.
SWR (Vercel)~4kBLanding pages dinâmicas, projetos simples de Next.jsExtremamente leve, API minimalista, otimizado para revalidação rápida.
RTK QueryIncluso no ReduxProjetos corporativos grandes que já utilizam Redux ToolkitCentralização do estado e caching integrados no mesmo store Redux.
Apollo Client~30kBProjetos focados 100% em infraestrutura GraphQLCache normalizado de objetos GraphQL e geração de queries robustas.

Fonte dos pesos das bibliotecas (Gzipped): Bundlephobia.


Regra de Decisão (Sem Dogma)#

Caso de usoFerramenta recomendadaJustificativa técnica
Consumo de APIs REST/GraphQLReact QueryEstado remoto com cache, deduplicação e revalidação
Sincronização de estado de servidorReact QueryReduz complexidade de Context/Redux para dados externos
Listeners de browser (resize/scroll)useEffectEfeito colateral de runtime, não estado remoto
Timers/intervalsuseEffectControle de ciclo de vida e limpeza (clearInterval)
Integração DOM/ref/lib externauseEffectOrquestração local do componente

Alternativas no Ecossistema (SWR, RTK Query)#

Embora o TanStack Query seja o padrão de facto na comunidade React, vale notar outras ferramentas e abordagens equivalentes:

  • SWR (Vercel): Extremamente leve e simples, ideal para aplicações menores. Possui foco na política Stale-While-Revalidate nativa.
  • RTK Query (Redux Toolkit): Recomendado caso sua aplicação já use Redux como governança de estado local abrangente. Evita carregar duas bibliotecas de estado robustas simultaneamente.
  • Apollo Client: Solução madura para ecossistemas 100% voltados a GraphQL com cache normativo integrado.

10. Referência Rápida de Estados, Checklist Operacional e Conclusão#

Referência Rápida de Estados de Consulta#

EstadoHookFinalidade Operacional
isLoadinguseQueryPrimeira busca iniciada (nenhum dado em cache está disponível)
isFetchinguseQueryRequisição ativa em andamento (independente de ter cache ou ser refetch)
isPendinguseMutationExecução da mutação de escrita ativa no servidor
isSuccessAmbosAção concluída com dados persistidos ou retornados
errorAmbosExceção capturada na promessa da chamada HTTP

isLoading vs isFetching - a diferença prática#

  • isLoading é true apenas na primeira busca, quando não há absolutamente nenhum dado em cache para essa query key. Use para renderizar o skeleton ou spinner inicial da página.
  • isFetching é true sempre que há qualquer requisição em andamento - inclusive refetches em background, polling por interval, ou refetch por foco de janela. Use para exibir um indicador sutil (ex: spinner pequeno no canto) sem esconder os dados já exibidos.
// Padrão correto para UX progressiva:
if (isLoading) return <FullPageSkeleton />; // Primeira carga: sem dado algum
return (
  <div>
    {isFetching && <SmallSpinner />} {/* Revalidando em background */}
    <UserList data={data} />
  </div>
);

11. Perguntas Frequentes (FAQ): useEffect vs TanStack Query#

Quando ainda devo usar o useEffect em vez do React Query?#

Devo usar o useEffect para sincronizar componentes com efeitos colaterais locais do navegador que não envolvem estado remoto de servidores. Exemplos diretos incluem: escutar eventos globais de redimensionamento de janela ou rolagem (window.addEventListener('resize')), configurar intervalos e timers que precisam de limpeza (setInterval / clearInterval), manipular diretamente referências do DOM (ref.current.focus()), ou inicializar bibliotecas externas imperativas de terceiros (como players de vídeo ou gráficos canvas locais). Para qualquer dado vindo de APIs HTTP, REST ou GraphQL, o TanStack Query é a escolha recomendada.

O TanStack Query substitui bibliotecas de estado global como Zustand ou Redux?#

Na maior parte das aplicações web modernas, sim. Em meus projetos e diagnósticos de arquitetura, observo que cerca de 80% do estado que desenvolvedores colocam no Redux ou Zustand é puramente estado remoto de servidor (dados de usuários, listas de produtos, pedidos, configurações da API). Ao delegar o estado remoto e o cache para o TanStack Query, as bibliotecas de estado global ficam reservadas apenas para estado do cliente (como tema escuro/claro ativo, abas selecionadas, carrinhos locais não salvos ou modais abertos), simplificando drasticamente a base de código.

Como o TanStack Query previne race conditions durante buscas com filtros rápidos?#

O TanStack Query indexa cada busca através de uma queryKey única (por exemplo, ['users', filterText]). Quando o usuário digita rapidamente e altera o filtro, uma nova chave de consulta é criada imediatamente. Se a requisição anterior ainda estiver transitando na rede, o TanStack Query desconsidera o resultado da chave anterior ou cancela a requisição ativa via AbortSignal automático (caso você passe o sinal no queryFn), garantindo que apenas a resposta da busca mais recente seja entregue ao componente.

Qual é a diferença prática entre isLoading e isFetching na experiência do usuário?#

Em termos de experiência de usuário (UX), a distinção é fundamental: isLoading é verdadeiro apenas na primeira busca daquela chave, quando não há absolutamente nenhum dado em cache (momento ideal para exibir um skeleton ou placeholder de tela cheia). Já o isFetching é verdadeiro sempre que qualquer chamada de rede está em andamento para aquela query, incluindo revalidações em segundo plano ao refocar a aba. Utilizo o isFetching para mostrar um pequeno spinner discreto no canto da interface, sem nunca ocultar o conteúdo que o usuário já está visualizando.

O React Query adiciona peso excessivo ao bundle final da aplicação?#

Não. O pacote @tanstack/react-query é altamente modular e possui suporte completo a tree-shaking moderno. Quando empacotado para produção, ele adiciona aproximadamente 12 a 13 KB (gzipped) ao bundle JavaScript da aplicação. Esse custo mínimo de transferência é amplamente compensado pela eliminação imediata de dezenas de hooks customizados, dependências de gerenciamento de estado redundantes e linhas repetitivas de código manual de fetch e tratamento de erros.


Considerações Finais de Arquitetura#

O useEffect para fetch de dados funciona como um protótipo, mas falha ao escalar. Para aplicações React modernas e de nível empresarial, adotar o TanStack Query reduz o código de estado redundante, melhora a experiência de navegação do usuário através de cache e poling seguro, e elimina bugs crônicos de concorrência.


Artigos Relacionados e Versão em Inglês: - Leia a versão completa em inglês deste guia: TanStack Query vs. useEffect: The Definitive Guide to Data Fetching, State Governance, and React Performance. - Se você também trabalha com ecossistemas fullstack modernos, recomendo ler meu guia prático: Do Caos à Ordem: Jornada Resolvendo Logs no Next.js 16 Turbopack. - Para telemetria de microsserviços e governança de requisições em tempo real, conheça o NEXUS Platform.

Este artigo foi útil?

Deixe uma reação rápida para apoiar o conteúdo:

810 leituras
Creative Commons Atribuição-NãoComercial

Este post está licenciado sob CC BY-NC.

Comentários

Participe da discussão abaixo.

0 comentários