Padrões avançados: dependent, parallel, infinite queries, suspense
5 min de leitura
Os quatro nós anteriores cobriram o "caminho feliz" do
TanStack Query: uma query por hook, uma mutation por vez,
e invalidação de cache. Mas apps reais têm casos
específicos que precisam de padrões dedicados: queries
que dependem de outras (pegar perfil → pegar projetos
dele), queries que precisam rodar em paralelo (dashboard
com 5 cards), listas que paginam (feed infinito), e a
integração com <Suspense> do React 18+.
Este nó cobre os quatro padrões que mais aparecem em produção, e como decidir qual usar em cada caso.
O essencial 🟢
Dependent queries: a query B precisa do resultado de A.
Padrão clássico: primeiro você busca o usuário, depois os
posts dele. O problema: a segunda query precisa do id
do usuário, que não existe até a primeira voltar.
A solução é o enabled: false que vimos no nó 2,
combinado com checagem explícita:
function PerfilComPosts({ userId }: { userId: string }) {
// Primeira query: dados do usuário
const { data: usuario } = useQuery({
queryKey: ["usuarios", userId],
queryFn: () => fetch(`/api/usuarios/${userId}`).then((r) => r.json()),
});
// Segunda query: SÓ roda quando o usuario.id existir
const { data: posts } = useQuery({
queryKey: ["usuarios", userId, "posts"],
queryFn: () => fetch(`/api/usuarios/${userId}/posts`).then((r) => r.json()),
enabled: !!usuario, // só roda se a primeira já resolveu
});
if (!usuario) return <Spinner />;
return (
<div>
<h1>{usuario.nome}</h1>
{/* posts ainda pode ser undefined se a segunda está carregando */}
{posts ? <ListaPosts posts={posts} /> : <SpinnerPequeno />}
</div>
);
}
A regra: enabled recebe uma condição. Se for false,
TanStack Query nem faz a request. Quando vira true (no
caso acima, quando usuario resolve), a request dispara.
Cuidado com timing. Se você colocar enabled: !!usuario.id
em vez de !!usuario, o usuario está resolvido mas pode
não ter id ainda (depende do formato da resposta). Use a
condição mais específica:
// Bom: dependência explícita
enabled: !!usuario?.id
// Ruim: usuário pode estar resolvido mas com erro
enabled: !!usuario // se for null em caso de erro, dispara a segunda
Parallel queries: várias queries independentes ao mesmo tempo. Quando a tela tem 5 cards que puxam dados diferentes (dashboard), você quer disparar todas em paralelo, não em série. Duas formas:
- Múltiplos
useQueryno mesmo componente - TanStack Query já dispara em paralelo automaticamente (cadauseQueryé independente). useQueriespra número dinâmico - quando a lista de queries vem de um array (ex: 10 usuários, cada um com seu detalhe).
// Múltiplos useQuery - paralelo automático
function Dashboard() {
const usuarios = useQuery({ queryKey: ["usuarios"], queryFn: ... });
const projetos = useQuery({ queryKey: ["projetos"], queryFn: ... });
const tarefas = useQuery({ queryKey: ["tarefas"], queryFn: ... });
if (usuarios.isLoading) return <Spinner />;
return (
<div>
<Card titulo="Usuários" data={usuarios.data} />
<Card titulo="Projetos" data={projetos.data} />
<Card titulo="Tarefas" data={tarefas.data} />
</div>
);
}
useQueries quando o número de queries é dinâmico:
function ListaDePosts({ userIds }: { userIds: string[] }) {
const queries = useQueries({
queries: userIds.map((id) => ({
queryKey: ["usuarios", id],
queryFn: () => fetch(`/api/usuarios/${id}`).then((r) => r.json()),
})),
});
const carregando = queries.some((q) => q.isLoading);
if (carregando) return <Spinner />;
return (
<ul>
{queries.map((q, i) => (
<li key={userIds[i]}>{q.data.nome}</li>
))}
</ul>
);
}
useQueries devolve um array com o resultado de cada
query, na mesma ordem. O some(isLoading) indica se
alguma ainda está carregando.
Árvore de decisão: qual padrão usar. Pra fixar:
Infinite queries: listas que paginam pra "carregar mais".
O useInfiniteQuery é um useQuery que sabe lidar com
páginas. O queryFn recebe um pageParam (parâmetro da
próxima página), e o hook devolve fetchNextPage (carrega
mais) e hasNextPage (tem mais?).
import { useInfiniteQuery } from "@tanstack/react-query";
type Page = {
items: Post[];
nextCursor: string | null;
};
function Feed() {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useInfiniteQuery({
queryKey: ["posts"],
queryFn: ({ pageParam }): Promise<Page> =>
fetch(`/api/posts?cursor=${pageParam ?? ""}`).then((r) => r.json()),
initialPageParam: null as string | null,
getNextPageParam: (ultimaPagina) => ultimaPagina.nextCursor,
});
return (
<div>
{data?.pages.map((pagina) =>
pagina.items.map((post) => <Post key={post.id} post={post} />),
)}
{hasNextPage && (
<button
onClick={() => fetchNextPage()}
disabled={isFetchingNextPage}
>
{isFetchingNextPage ? "Carregando..." : "Carregar mais"}
</button>
)}
</div>
);
}
As três peças importantes:
queryFn: ({ pageParam })- recebe o cursor da próxima página. Na primeira chamada,pageParamé oinitialPageParam. Nas seguintes, é o quegetNextPageParamretornou.getNextPageParam: (ultimaPagina) => ...- diz pro TanStack Query "a próxima página é X, ou null se acabou". O retorno é o que vira opageParamda próxima request.data.pages- array de páginas.data.pages[0]é a primeira,data.pages[1]a segunda, etc. Você não concatena automaticamente - a UI faz isso no.map.
useSuspenseQuery: data fetching com Suspense. O React
18+ introduziu <Suspense> como first-class pra data
fetching. useSuspenseQuery joga uma promise (em vez de
retornar isLoading: true), e o componente pai usa
<Suspense fallback={...}> pra mostrar loading:
import { useSuspenseQuery } from "@tanstack/react-query";
import { Suspense } from "react";
function Perfil({ userId }: { userId: string }) {
// Não tem isLoading - ou data está pronto ou joga promise
const { data: usuario } = useSuspenseQuery({
queryKey: ["usuarios", userId],
queryFn: () => fetch(`/api/usuarios/${userId}`).then((r) => r.json()),
});
// data é garantido não-undefined aqui
return <h1>{usuario.nome}</h1>;
}
function App() {
return (
<Suspense fallback={<Spinner />}>
<Perfil userId="u-1" />
</Suspense>
);
}
A grande vantagem: elimina if (isLoading) return <Spinner />
de cada componente. O <Suspense> na borda cuida de todos
os loadings aninhados. Erros são tratados por <ErrorBoundary>
(o Suspense não trata erro - é outra primitiva).
A desvantagem: você precisa de <ErrorBoundary> (não nativo
do React ainda, mas trivial com libs) pra erros. E o
refactor de "useQuery pra useSuspenseQuery" exige mover a
lógica de loading pra cima na árvore.
Quando usar useSuspenseQuery vs useQuery. Regra
prática:
- Use
useQueryem código legado, ou onde você precisa deisLoadingseparado deisError(ex: componente que mostra "X itens encontrados" em vez de spinner cheio). - Use
useSuspenseQueryem código novo do React 18+, especialmente em apps que já usam Suspense pra outras coisas (lazy components, etc). É a recomendação oficial do time do TanStack Query.
Aprofundamento 🟡
select + useQueries pra agregar dados de várias
queries. Às vezes você quer pegar dados de várias queries
e transformar num único objeto. select resolve:
const { data: resumo } = useQueries({
queries: [
{ queryKey: ["usuarios"], queryFn: fetchUsuarios },
{ queryKey: ["projetos"], queryFn: fetchProjetos },
{ queryKey: ["tarefas"], queryFn: fetchTarefas },
],
combine: (results) => ({
usuarios: results[0].data,
projetos: results[1].data,
tarefas: results[2].data,
carregando: results.some((r) => r.isLoading),
}),
});
O combine (em vez de select) é o padrão novo: roda
uma vez quando todas as queries resolvem, em vez de
em cada mudança individual. Evita re-renders desnecessários
em dashboards com muitas queries.
Infinite queries: select pra filtrar ou transformar
páginas. O data.pages é um array, e cada página tem
a estrutura que o backend devolveu. Pra UX, às vezes
você quer "achatar" tudo num único array:
const { data: todosOsPosts } = useInfiniteQuery({
queryKey: ["posts"],
queryFn: fetchPage,
initialPageParam: null,
getNextPageParam: (ultima) => ultima.nextCursor,
select: (data) => ({
...data,
items: data.pages.flatMap((p) => p.items),
}),
});
// Uso: todosOsPosts.items é um array flat de todos os posts
todosOsPosts?.items.map((p) => <Post key={p.id} post={p} />)
Atenção: o select cria um novo objeto a cada render.
Se a lista for muito grande (10k+ itens), considere
memoizar ou paginar de outra forma.
Prefetch pra UX instantânea. Antes do usuário navegar pra uma rota, dá pra "esquentar" o cache:
function LinkParaPerfil({ userId }: { userId: string }) {
const queryClient = useQueryClient();
return (
<Link
href={`/usuarios/${userId}`}
onMouseEnter={() => {
// Pré-carrega quando o mouse passa por cima
queryClient.prefetchQuery({
queryKey: ["usuarios", userId],
queryFn: () => fetch(`/api/usuarios/${userId}`).then((r) => r.json()),
});
}}
>
Ver perfil
</Link>
);
}
Quando o usuário clicar, o cache já tem o dado e a página abre sem loading. Padrão "mágico" que aparece em apps como Gmail e Notion.
useMutationState + mutationKey pra UI global de
mutations. Quando várias mutations disparam de
componentes diferentes, dá pra ter um indicador global
"saving...":
// No header da app
const mutations = useMutationState({
filters: { status: "pending" },
select: (m) => m.options.mutationKey,
});
const temMutation = mutations.length > 0;
return (
<header>
{temMutation && <IndicadorSalvando />}
</header>
);
Útil pra feedback unificado em apps com formulários espalhados.
Pra quem quer ir além 🔴
useQueries com combine - a evolução do parallel.
O combine (introduzido no TanStack Query 5.x) é o
sucessor do select pra casos complexos. Em vez de mapear
cada query, você compõe o resultado final numa única
função, e o React só re-renderiza quando o resultado
muda (graças ao structural sharing interno).
Infinite queries com scroll infinito. O exemplo com
botão "Carregar mais" é o começo. O padrão mais usado em
produção é scroll infinito via IntersectionObserver:
const observerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!observerRef.current || !hasNextPage) return;
const observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) fetchNextPage();
});
observer.observe(observerRef.current);
return () => observer.disconnect();
}, [fetchNextPage, hasNextPage]);
return (
<div>
{data?.pages.map(...)}
{hasNextPage && <div ref={observerRef}>Carregando...</div>}
</div>
);
Aprofundamento de UX aqui pertence a
performance-web (issue #48) - o IntersectionObserver
em si é uma API do browser, não do TanStack Query.
useSuspenseQueries (paralelo + Suspense). Versão
suspense-aware de useQueries, mesma ideia do
useSuspenseQuery mas pra múltiplas queries.
Streaming queries com experimental_streamedQuery.
TanStack Query 5.x adicionou suporte experimental pra
streaming (Server-Sent Events, AI streaming). Aprofundamento
fica pra real-time-websockets-sse (issue #52) e
engenharia-assistida-por-ia (issue #55).
Leitura recomendada:
- TanStack Query - Dependent Queries (oficial) - o básico, bem coberto.
- TanStack Query - Infinite Queries (oficial) - a referência completa, com exemplos de cursor e offset.
- TkDodo - Practical React Query 9: Infinite Queries (vídeo) - passo a passo.
Dica: o erro mais comum em dependent queries é esquecer o
enabled: falsequando o ID é null. O sintoma: a request quebra com "Cannot read property 'id' of null". A correção é simples:enabled: !!userId && userId !== "", ou no caso mais comumenabled: !!usuario?.idna segunda query.
No próximo nó, vamos ao debugging: como usar o React Query Devtools pra entender o que está no cache, o que cada query está fazendo, e os erros mais comuns em produção.
// Quiz
Quando você tem uma lista de IDs (ex: 10 userIds) e quer buscar o detalhe de cada um em paralelo, qual hook você usa?