Consulta TanStack vs SWR: Guia do roteador de aplicativos Next.js


Comparação de TanStack Query vs SWR

DimensãoConsulta TanStack v5ROE 2.x
Tamanho do pacote (compactado)~7 kB (núcleo + useQuery/useMutation)~5,5kB
Suporte a mutaçãoIntegrado useMutation com onMutate/onError/onSettled vida útiluseSWRMutation (de swr/mutation) ou manual fetch + mutate()
Reversão otimistaExplícito onMutate→onError fluxo de contexto; suporta restauração multi-consultaDeclarativo optimisticData + rollbackOnError; conciso para alternâncias simples
Ferramentas de desenvolvimentoDevtools originais oficiais com cronograma de cache, inspeção de consulta e invalidação manualCriado pela comunidade (swr-devtools), menos rico em recursos

A maioria das comparações entre TanStack Query e SWR foram escritas para a era do Pages Router, onde cada página dependia de getServerSideProps ou getStaticProps e as bibliotecas de busca do lado do cliente carregavam todo o peso do gerenciamento de dados. O Next.js App Router, com seus padrões de componente de servidor, muda fundamentalmente esse cálculo. Este guia fornece uma estrutura de decisão baseada em cenário para escolher entre TanStack Query e SWR em projetos de App Router, em vez de refazer listas de verificação de recursos.

Índice

Componentes de servidor versus componentes de cliente: a divisão de busca de dados

O que os componentes do servidor tratam por padrão

No App Router, layouts e páginas são componentes do servidor por padrão. Eles podem async/await dados diretamente, chamando bancos de dados, APIs internas ou serviços de terceiros sem enviar qualquer JavaScript para o navegador. Props buscados pelo servidor e React.use() o streaming dos dados iniciais da página de rosto precisa de forma eficaz (estável no React 19; canário no React 18). Como afirma a documentação do Next.js, os componentes do servidor são renderizados no servidor e não carregam nenhuma sobrecarga de interatividade do lado do cliente, tornando-os o padrão correto para quaisquer dados que você conhece no momento da solicitação que não dependem da interação do usuário.

Quando você ainda precisa de uma biblioteca do lado do cliente

Os dados de tempo de interação são onde as bibliotecas do lado do cliente ganham seu lugar. Filtros de pesquisa, rolagem infinita, conteúdo modal sob demanda e qualquer busca que dependa do estado do usuário ou de um evento do navegador pertencem a um componente cliente com uma biblioteca de busca. O conteúdo carregado apenas quando um usuário abre um modal é um exemplo típico. A regra prática: se o gatilho for um clique, um pressionamento de tecla ou uma posição de rolagem, procure uma biblioteca cliente. Se você conhece os dados no momento da solicitação, deixe um componente de servidor lidar com eles e ignore completamente a biblioteca.

A regra prática: se o gatilho for um clique, um pressionamento de tecla ou uma posição de rolagem, procure uma biblioteca cliente. Se você conhece os dados no momento da solicitação, deixe um componente de servidor lidar com eles e ignore completamente a biblioteca.

Consulta TanStack vs SWR em resumo

Filosofia da API e tamanho do pacote

SWR segue uma filosofia mínima centrada no gancho. Com aproximadamente 5,5 kB compactado quando incluído no esbuild (SWR 2.5), ele prioriza a convenção sobre a configuração. Um único useSWR hook lida com a grande maioria dos casos de uso, e a superfície da API é deliberadamente pequena. O TanStack Query adota a abordagem oposta: rico em recursos e explícito, pesando aproximadamente 7 kB compactado para o cliente mais useQuery, useMutation e o cliente de consulta (TanStack Query 5.104, fornecido com esbuild, portanto seu próprio número variará com as importações). Ele oferece controle granular sobre a vida útil do cache, novas tentativas, coleta de lixo e ciclo de vida da consulta. A lacuna é pequena, cerca de 1-2 kB compactada, portanto, o tamanho do pacote por si só raramente deve decidir a escolha.

Paridade e divergência de recursos

Ambas as bibliotecas fornecem cache obsoleto enquanto revalidam, solicitam desduplicação, busca de foco e suporte à hidratação SSR. Além desse terreno comum, eles divergem.

O TanStack Query fornece mutações integradas via useMutation com onMutate/onError/onSettled ganchos de ciclo de vida, cancelamento de consulta e select transformações para dados derivados. Também envia placeholderData para transições suaves e ferramentas de desenvolvimento oficiais com inspeção de estado de consulta, visualização de linha de tempo de cache e controles manuais de invalidação. Contadores SWR com um modelo mental mais simples, useSWRInfinite para paginação e uma árvore de dependências mais leve. O ROE tem useSWRMutation de swr/mutation para gravações remotas, mas é uma API mais fina, sem onMutate fluxo de contexto. Os exemplos abaixo usam simples fetch mais mutateque é a opção de nível mais baixo.

Tabela de decisão de cenário para abordagem

Esta tabela mapeia cenários comuns de busca de dados para a abordagem recomendada e, quando uma biblioteca cliente for garantida, para a opção preferida.

CenárioAbordagem recomendadaBiblioteca de Cliente PreferencialPor que
Dados de página estática/SSGBusca de componentes do servidorNenhumNenhum JS enviado ao cliente
Painel de sessão do usuário (carregamento inicial)Componente de servidor + streamingNenhumRenderizar no servidor, transmitir para o cliente
Pesquise com filtros/facetasComponente ClienteQualquer; O TanStack Query destaca-se pelo gerenciamento complexo de chavesAs chaves de consulta são mapeadas naturalmente para o estado do filtro
Rolagem/paginação infinitaComponente ClienteROE (useSWRInfinite) ou Consulta TanStack (useInfiniteQuery)Ambos capazes; API SWR um pouco mais simples
Busca de detalhes modal sob demandaComponente Cliente (preguiçoso)Qualquer; ROE suficienteBusca condicional, carga útil pequena
Envio de formulário + atualização de cacheMutação do componente clienteConsulta TanStackIntegrado useMutation com reversão otimista
Dados em tempo real/pesquisaComponente ClienteQualquerAmbos suportam refreshInterval / refetchInterval
Off-line primeiro com nova tentativaComponente ClienteConsulta TanStackNova tentativa configurável e networkMode opções (SWR também tenta novamente em caso de erro)

Esta tabela foi concebida como um ponto de partida de referência rápida. Os exemplos de código emparelhados abaixo demonstram três dos padrões mais críticos na prática.

Exemplos de códigos emparelhados: três padrões críticos

Pré-requisitos

Esses exemplos exigem Próximo.js 13.4+, Reagir 18+, @tanstack/react-query ^5,0, swr ^2,0e Node.js 18+. Instale as bibliotecas cliente:

npm install @tanstack/react-query@^5 @tanstack/react-query-devtools@^5 swr@^2

Todos os padrões de código assumem que existem rotas de API stub em /api/products, /api/reviewse /api/products/(id) retornando JSON com formato apropriado.

Cada subseção abaixo implementa o mesmo cenário lado a lado no TanStack Query e SWR dentro de um componente cliente Next.js App Router usando o "use client" diretiva.

Padrão 1: chaves de consulta filtradas

Cenário: Uma página de catálogo de produtos onde o usuário alterna categoria e classificação de filtros.

Consulta TanStack

"use client";


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

async function fetchProducts({ queryKey, signal }) {
  const (, { category, sort }) = queryKey;
  const params = new URLSearchParams({ category, sort });
  const res = await fetch(`/api/products?${params}`, { signal });
  if (!res.ok) throw new Error("Failed to fetch products");
  return res.json();
}

export default function ProductCatalog() {
  const (category, setCategory) = useState("all");
  const (sort, setSort) = useState("newest");

  const { data, isLoading, isFetching, isPlaceholderData, error } = useQuery({
    queryKey: ("products", { category, sort }),
    queryFn: fetchProducts,
    placeholderData: keepPreviousData,
  });

  if (error) return <p role="alert">Failed to load products: {error.message}p>;

  return (
    <div>
      <select value={category} onChange={(e) => setCategory(e.target.value)}>
        <option value="all">Alloption>
        <option value="electronics">Electronicsoption>
        <option value="clothing">Clothingoption>
      select>

      <select value={sort} onChange={(e) => setSort(e.target.value)}>
        <option value="newest">Newestoption>
        <option value="price-asc">Price: Low to Highoption>
      select>

      {isLoading ? (
        <p>Loading...p>
      ) : (
        <ul style={{ opacity: isPlaceholderData || isFetching ? 0.5 : 1 }}>
          {data?.map((p) => (
            <li key={p.id}>{p.name}li>
          ))}
        ul>
      )}
    div>
  );
}

Cabo de aço

"use client";

import useSWR from "swr";
import { useState } from "react";

const fetcher = async (...args) => {
  const res = await fetch(...args);
  if (!res.ok) {
    const error = new Error("API request failed");
    error.status = res.status;
    error.info = await res.json().catch(() => null);
    throw error;
  }
  return res.json();
};

export default function ProductCatalog() {
  const (category, setCategory) = useState("all");
  const (sort, setSort) = useState("newest");

  const swrKey = `/api/products?${new URLSearchParams({ category, sort })}`;
  const { data, isLoading, isValidating, error } = useSWR(swrKey, fetcher, {
    keepPreviousData: true, 
  });

  if (error) return <p role="alert">Failed to load products: {error.message}p>;

  return (
    <div>
      <select value={category} onChange={(e) => setCategory(e.target.value)}>
        <option value="all">Alloption>
        <option value="electronics">Electronicsoption>
        <option value="clothing">Clothingoption>
      select>

      <select value={sort} onChange={(e) => setSort(e.target.value)}>
        <option value="newest">Newestoption>
        <option value="price-asc">Price: Low to Highoption>
      select>

      {isLoading ? (
        <p>Loading...p>
      ) : (
        <ul style={{ opacity: isValidating ? 0.5 : 1 }}>
          {data?.map((p) => (
            <li key={p.id}>{p.name}li>
          ))}
        ul>
      )}
    div>
  );
}

Chaves de matriz estruturada e integradas do TanStack Query select Essa opção facilita o gerenciamento de combinações complexas de filtros em escala, especialmente quando vários componentes consomem os mesmos dados com diferentes transformações. A abordagem do SWR é mais enxuta para casos simples em que uma string de URL serializada é suficiente como chave de cache.

Padrão 2: invalidação de cache após mutação

Cenário: Um usuário envia uma avaliação; a lista de avaliações deve ser atualizada.

Consulta TanStack

"use client";

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

export default function ReviewSection({ productId }) {
  const queryClient = useQueryClient();

  const { data: reviews, error } = useQuery({
    queryKey: ("reviews", productId),
    queryFn: () => {
      const params = new URLSearchParams({ productId: String(productId) });
      return fetch(`/api/reviews?${params}`).then((r) => {
        if (!r.ok) throw new Error("Failed to fetch reviews");
        return r.json();
      });
    },
  });

  const mutation = useMutation({
    mutationFn: async (newReview) => {
      const res = await fetch("/api/reviews", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ productId, ...newReview }),
      });
      if (!res.ok) throw new Error(await res.text());
      return res.json();
    },
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ("reviews", productId) });
    },
  });

  if (error) return <p role="alert">Failed to load reviews: {error.message}p>;

  return (
    <div>
      <ul>
        {reviews?.map((r) => (
          <li key={r.id}>{r.text}li>
        ))}
      ul>

      <button
        onClick={() => mutation.mutate({ text: "Great product!", rating: 5 })}
        disabled={mutation.isPending}
      >
        {mutation.isPending ? "Submitting..." : "Submit Review"}
      button>

      {mutation.isError && <p role="alert" style={{ color: "red" }}>{mutation.error.message}p>}
    div>
  );
}

Cabo de aço

"use client";

import useSWR, { useSWRConfig } from "swr";
import { useState } from "react";

const fetcher = async (url) => {
  const res = await fetch(url);
  if (!res.ok) {
    const error = new Error("API request failed");
    error.status = res.status;
    error.info = await res.json().catch(() => null);
    throw error;
  }
  return res.json();
};

export default function ReviewSection({ productId }) {
  const { mutate } = useSWRConfig();
  const reviewKey = `/api/reviews?${new URLSearchParams({ productId: String(productId) })}`;
  const { data: reviews, error: fetchError } = useSWR(reviewKey, fetcher);

  const (isPending, setIsPending) = useState(false);
  const (submitError, setSubmitError) = useState(null);

  async function submitReview() {
    if (isPending) return; 
    setIsPending(true);
    setSubmitError(null);

    try {
      const res = await fetch("/api/reviews", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ productId, text: "Great product!", rating: 5 }),
      });
      if (!res.ok) throw new Error(`Submit failed: ${res.status}`);
      await mutate(reviewKey);
    } catch (err) {
      setSubmitError(err.message);
    } finally {
      setIsPending(false);
    }
  }

  if (fetchError) return <p role="alert">Failed to load reviews: {fetchError.message}p>;

  return (
    <div>
      <ul>
        {reviews?.map((r) => (
          <li key={r.id}>{r.text}li>
        ))}
      ul>

      <button onClick={submitReview} disabled={isPending}>
        {isPending ? "Submitting..." : "Submit Review"}
      button>

      {submitError && <p role="alert" style={{ color: "red" }}>{submitError}p>}
    div>
  );
}

Ganchos do ciclo de vida de mutação do TanStack Query (onSuccess, onError, isPending) reduzem o padrão para formulários que precisam de estados de carregamento, erro e sucesso. SWR mantém as coisas mais próximas do bruto fetchque é simples, mas requer gerenciamento manual de estado para estados pendentes e de erro, conforme mostrado no useState guarda acima.

Ganchos do ciclo de vida de mutação do TanStack Query (onSuccess, onError, isPending) reduzem o padrão para formulários que precisam de estados de carregamento, erro e sucesso.

Padrão 3: atualizações otimistas com reversão

Cenário: Um usuário alterna um ícone de coração “favorito”; a IU é atualizada instantaneamente e revertida em caso de falha.

Consulta TanStack

"use client";

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

export default function FavoriteButton({ productId }) {
  const queryClient = useQueryClient();

  const { data: product } = useQuery({
    queryKey: ("product", productId),
    queryFn: () =>
      fetch(`/api/products/${productId}`).then((r) => {
        if (!r.ok) throw new Error("Failed to fetch product");
        return r.json();
      }),
  });

  const toggleFav = useMutation({
    mutationFn: async () => {
      const res = await fetch(`/api/products/${productId}/favorite`, { method: "POST" });
      if (!res.ok) throw new Error(await res.text());
      return res.json();
    },
    onMutate: async () => {
      await queryClient.cancelQueries({ queryKey: ("product", productId) });
      const previous = queryClient.getQueryData(("product", productId));

      queryClient.setQueryData(("product", productId), (old) => {
        if (!old) return old; 
        return { ...old, isFavorited: !old.isFavorited };
      });

      return { previous };
    },
    onError: (_err, _vars, context) => {
      queryClient.setQueryData(("product", productId), context.previous);
    },
    onSettled: () => {
      queryClient.invalidateQueries({ queryKey: ("product", productId) });
    },
  });

  return (
    <button onClick={() => toggleFav.mutate()}>
      {product?.isFavorited ? "♥" : "♡"}
    button>
  );
}

Cabo de aço

"use client";

import useSWR from "swr";

const fetcher = async (url) => {
  const res = await fetch(url);
  if (!res.ok) {
    const error = new Error("API request failed");
    error.status = res.status;
    error.info = await res.json().catch(() => null);
    throw error;
  }
  return res.json();
};

export default function FavoriteButton({ productId }) {
  const { data: product, mutate } = useSWR(
    `/api/products/${productId}`,
    fetcher
  );

  async function toggleFav() {
    await mutate(
      async (current) => {
        const res = await fetch(`/api/products/${productId}/favorite`, { method: "POST" });
        if (!res.ok) throw new Error("Favorite toggle failed");
        return { ...current, isFavorited: !current.isFavorited };
      },
      {
        optimisticData: (current) => ({
          ...current,
          isFavorited: !current.isFavorited,
        }),
        rollbackOnError: true,
        revalidate: true,
      }
    );
  }

  return (
    <button onClick={toggleFav}>{product?.isFavorited ? "♥" : "♡"}button>
  );
}

Ambas as bibliotecas suportam UI otimista, mas o explícito do TanStack Query onMutate para onError para onSettled O ciclo de vida oferece controle mais preciso para cenários de reversão complexos, como quando várias consultas precisam de restauração. Declarativo de SWR rollbackOnError combinado com optimisticData é mais conciso para alternâncias diretas. Observe que rollbackOnError só é ativado quando a função mutadora gera um erro. O if (!res.ok) throw verificar acima é essencial, pois fetch resolve com sucesso mesmo em respostas HTTP 4xx/5xx.

Configurando provedores no App Router

Provedor de consulta TanStack

"use client";

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

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

const DEFAULT_STALE_MS = 60_000;

export default function QueryProvider({ children }) {
  const (queryClient) = useState(
    () =>
      new QueryClient({
        defaultOptions: {
          queries: {
            staleTime: DEFAULT_STALE_MS, 
          },
        },
      })
  );

  return (
    <QueryClientProvider client={queryClient}>
      {children}
      <ReactQueryDevtools initialIsOpen={false} />
    QueryClientProvider>
  );
}

}

Importe este componente para o layout raiz e envolva {children} com isso. O useState inicializador garante um único QueryClient instância por renderização do lado do cliente, evitando estado compartilhado entre solicitações durante SSR. Definir um staleTime o padrão evita que o cliente recupere imediatamente dados que já foram pré-buscados no servidor, o que, de outra forma, causaria uma busca dupla de dados. O pacote devtools não renderiza nada, a menos que NODE_ENV é developmentpara que o componente possa permanecer na árvore para compilações de produção.

Exemplo app/layout.tsx:

import QueryProvider from "@/components/QueryProvider";

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <QueryProvider>{children}QueryProvider>
      body>
    html>
  );
}

Provedor de SWR (opcional)

"use client";

import { SWRConfig } from "swr";

const fetcher = async (url) => {
  const res = await fetch(url);
  if (!res.ok) {
    const error = new Error("API request failed");
    error.status = res.status;
    error.info = await res.json().catch(() => null);
    throw error;
  }
  return res.json();
};

export default function SWRProvider({ children }) {
  return (
    <SWRConfig value={{ fetcher, revalidateOnFocus: true }}>
      {children}
    SWRConfig>
  );
}

SWR funciona sem provedor. O SWRConfig wrapper é opcional e existe principalmente para definir padrões globais como um arquivo compartilhado fetcher função e comportamento de revalidação, reduzindo a repetição entre componentes. Sem um provedor, todos os componentes compartilham o cache global padrão do SWR, e você pode chamar o cache global mutate exportado de swr em qualquer lugar. Adicione um SWRConfig ancestral somente quando você deseja um provedor de cache diferente ou padrões com escopo definido.

Considerações sobre desempenho, DX e ecossistema

Impacto do pacote

Em uma medição rápida de esbuild (minificado, compactado, React excluído), o SWR 2.5 chegou a cerca de 5,5 kB e o TanStack Query 5.104 com QueryClient, useQuery e useMutation para cerca de 7 kB. Meça sua própria construção, pois o resultado depende de quais ganchos você importa. A lacuna é muito pequena para decidir a escolha por conta própria. Ambas as bibliotecas suportam trepidação de árvores; a eficácia depende da configuração do bundler e das importações nomeadas (import { useQuery }) permitem melhor agitação da árvore do que importações de namespace.

Ferramentas de desenvolvimento

O TanStack Query fornece ferramentas de desenvolvimento oficiais com inspeção de estado de consulta, visualização da linha do tempo do cache e controles manuais de invalidação. Os devtools do SWR são um projeto comunitário (swr-devtools) e menos ricos em recursos. Para equipes que depuram interações complexas de cache em muitas chaves de consulta, os devtools do TanStack Query oferecem uma vantagem significativa de produtividade.

Experiência TypeScript

Ambas as bibliotecas fornecem definições completas de tipo. Genéricos do TanStack Query em useQuery e useMutation fornecem contratos de tipo mais rígidos, especialmente para ganchos de ciclo de vida de mutação de onde o tipo de contexto flui onMutate através onError. A tipagem do SWR funciona bem para casos simples, mas oferece menos granularidade nos fluxos de mutação.

Estrutura de decisão: como escolher

Escolha ROE quando as necessidades de busca de dados são simples: páginas com muita leitura com chaves de cache simples, mutações mínimas ou nenhuma e prioridade no tamanho mínimo do pacote. O SWR não requer nenhum provedor para ser iniciado, portanto, a configuração inicial é uma chamada de gancho único.

Quando seu aplicativo requer mutações estruturadas com reversão otimista, chaves de consulta multidimensionais, tempo de coleta de lixo ou cancelamento de consulta, TanStack Query é a melhor opção. Suas ferramentas de desenvolvimento originais com inspeção de estado de consulta e visualizações de cronograma de cache também ajudam as equipes a depurar interações complexas de cache com mais rapidez.

Não considere nenhum dos dois para dados iniciais de página que os componentes do servidor podem buscar diretamente. O App Router torna esse o caminho padrão, e adicionar uma biblioteca do lado do cliente para dados que não dependem da interação do usuário adiciona complexidade desnecessária e peso do pacote.

O App Router torna esse o caminho padrão, e adicionar uma biblioteca do lado do cliente para dados que não dependem da interação do usuário adiciona complexidade desnecessária e peso do pacote.

O App Router restringe a superfície

O App Router reduz a área de superfície onde qualquer biblioteca é necessária. Os componentes do servidor cuidam do trabalho de busca de dados que anteriormente exigia bibliotecas do lado do cliente por padrão. Para os cenários de tempo de interação que permanecem, o TanStack Query e o SWR cobrem bem o terreno, com diferentes perfis de compensação: o TanStack Query oferece mais estrutura para mutações e controle de cache; O SWR oferece menos sobrecarga para casos de uso de leitura pesada e de chave simples. A tabela de decisão e os exemplos de código emparelhados acima foram projetados como uma referência durável.



Source link