React & Frontend

Guia Completo de React Query Avançado: Cache, Stale Time, Prefetch e Optimistic UI

12 min de leitura

Guia Completo de React Query Avançado: Cache, Stale Time, Prefetch e Optimistic UI

Entendendo o Cache e sua Importância no React Query O cache é o coração do React Query. Diferentemente de outras bibliotecas que apenas gerenciam estado, o React Query mantém uma cópia dos dados já fetched no navegador, evitando requisições desnecessárias. Quando você faz uma query pela primeira vez, a resposta é armazenada em memória com uma chave única. Na próxima vez que essa query for necessária, o React Query retorna os dados do cache instantaneamente, sem fazer uma nova requisição HTTP. Para entender isso na prática, vamos ver como o cache funciona por padrão. Quando você cria uma query com , ela passa por diferentes estados: (primeira requisição), (busca em background), e finalmente retorna dados do cache. O cache também tem um conceito de "stale" (obsoleto), que é quando os dados precisam ser revalidados com o servidor. /api/users/${userId} Neste exemplo, após os primeiros 5 minutos ( ), os dados são considerados obsoletos. Mas observe: os dados ainda estão no cache,

<h2>Entendendo o Cache e sua Importância no React Query</h2>

<p>O cache é o coração do React Query. Diferentemente de outras bibliotecas que apenas gerenciam estado, o React Query mantém uma cópia dos dados já fetched no navegador, evitando requisições desnecessárias. Quando você faz uma query pela primeira vez, a resposta é armazenada em memória com uma chave única. Na próxima vez que essa query for necessária, o React Query retorna os dados do cache instantaneamente, sem fazer uma nova requisição HTTP.</p>

<p>Para entender isso na prática, vamos ver como o cache funciona por padrão. Quando você cria uma query com <code>useQuery</code>, ela passa por diferentes estados: <code>isLoading</code> (primeira requisição), <code>isFetching</code> (busca em background), e finalmente retorna dados do cache. O cache também tem um conceito de &quot;stale&quot; (obsoleto), que é quando os dados precisam ser revalidados com o servidor.</p>

<pre><code class="language-typescript">import { useQuery } from &#039;@tanstack/react-query&#039;;

const UserProfile = ({ userId }: { userId: string }) =&gt; {

const { data, isLoading, isFetching } = useQuery({

queryKey: [&#039;user&#039;, userId],

queryFn: async () =&gt; {

const response = await fetch(/api/users/${userId});

return response.json();

},

staleTime: 5 60 1000, // 5 minutos

});

if (isLoading) return &lt;div&gt;Carregando...&lt;/div&gt;;

return (

&lt;div&gt;

&lt;h1&gt;{data.name}&lt;/h1&gt;

{isFetching &amp;&amp; &lt;span&gt;(atualizando...)&lt;/span&gt;}

&lt;/div&gt;

);

};</code></pre>

<p>Neste exemplo, após os primeiros 5 minutos (<code>staleTime</code>), os dados são considerados obsoletos. Mas observe: os dados ainda estão no cache, apenas marcados como stale. Se o usuário navegar para outra página e voltar dentro da janela de <code>cacheTime</code>, os dados serão mostrados imediatamente enquanto uma nova requisição é feita em background.</p>

<h2>Dominando Stale Time e Cache Time</h2>

<p>Stale Time e Cache Time são dois conceitos distintos que precisam ser bem compreendidos. <strong>Stale Time</strong> define quanto tempo os dados são considerados &quot;frescos&quot;. Enquanto os dados não estão stale, nenhuma requisição é feita em background. <strong>Cache Time</strong> (renomeado para <code>gcTime</code> no TanStack Query v5) define por quanto tempo os dados permanecem na memória após não serem mais utilizados.</p>

<p>A configuração padrão do React Query é <code>staleTime: 0</code> e <code>gcTime: 5 <em> 60 </em> 1000</code> (5 minutos). Isso significa que os dados são imediatamente considerados obsoletos, mas permanecem na memória por 5 minutos após o último acesso. Se um usuário navegar para a página de perfil, depois para home, e retornar ao perfil dentro de 5 minutos, os dados serão mostrados do cache e uma requisição silenciosa será feita em background.</p>

<pre><code class="language-typescript">import { useQuery, useQueryClient } from &#039;@tanstack/react-query&#039;;

const ProductList = () =&gt; {

const queryClient = useQueryClient();

const { data: products } = useQuery({

queryKey: [&#039;products&#039;],

queryFn: async () =&gt; {

const response = await fetch(&#039;/api/products&#039;);

return response.json();

},

staleTime: 10 60 1000, // 10 minutos - dados ficam frescos por 10 min

gcTime: 15 60 1000, // 15 minutos - cache permanece por 15 min

});

const handleRefreshManual = () =&gt; {

// Força revalidação imediata

queryClient.invalidateQueries({ queryKey: [&#039;products&#039;] });

};

return (

&lt;div&gt;

&lt;button onClick={handleRefreshManual}&gt;Atualizar dados&lt;/button&gt;

{/ renderizar produtos /}

&lt;/div&gt;

);

};</code></pre>

<p>A estratégia correta depende do seu caso de uso. Para dados que mudam frequentemente (como notificações ou preços), use um <code>staleTime</code> menor. Para dados mais estáticos (como informações de um artigo), use um <code>staleTime</code> maior. O <code>gcTime</code> deve ser sempre maior que o <code>staleTime</code>, caso contrário o cache será limpo antes dos dados serem considerados stale.</p>

<h3>Invalidação Manual e Revalidação Inteligente</h3>

<p>Às vezes, você sabe que os dados ficaram obsoletos e precisa invalidá-los manualmente. O <code>queryClient.invalidateQueries()</code> marca queries como stale, forçando uma revalidação quando o componente tentar acessá-las. Isso é diferente de deletar o cache: os dados continuam disponíveis enquanto a nova requisição é feita.</p>

<pre><code class="language-typescript">import { useMutation, useQueryClient } from &#039;@tanstack/react-query&#039;;

const CreatePost = () =&gt; {

const queryClient = useQueryClient();

const createPostMutation = useMutation({

mutationFn: async (newPost: { title: string; content: string }) =&gt; {

const response = await fetch(&#039;/api/posts&#039;, {

method: &#039;POST&#039;,

headers: { &#039;Content-Type&#039;: &#039;application/json&#039; },

body: JSON.stringify(newPost),

});

return response.json();

},

onSuccess: (data) =&gt; {

// Invalida a lista de posts para que seja recarregada

queryClient.invalidateQueries({ queryKey: [&#039;posts&#039;] });

// Ou atualiza o cache diretamente (mais eficiente)

queryClient.setQueryData([&#039;posts&#039;], (oldData: any) =&gt; [

...oldData,

data,

]);

},

});

return (

&lt;button onClick={() =&gt; createPostMutation.mutate({ title: &#039;&#039;, content: &#039;&#039; })}&gt;

Criar Post

&lt;/button&gt;

);

};</code></pre>

<h2>Prefetch: Carregando Dados Antes da Necessidade</h2>

<p>Prefetch é uma técnica poderosa para melhorar a experiência do usuário. Ao invés de esperar o usuário clicar em um link ou navegar para uma página, você antecipa quais dados serão necessários e os carrega em background. Isso é particularmente útil em paginação, listagens onde o usuário provavelmente vai clicar no próximo item, ou em previsão de navegação.</p>

<p>O React Query fornece o <code>queryClient.prefetchQuery()</code> que faz uma requisição sem renderizar dados, apenas armazenando no cache. Se um componente depois usar <code>useQuery</code> com a mesma chave, os dados já estarão disponíveis.</p>

<pre><code class="language-typescript">import { useQueryClient } from &#039;@tanstack/react-query&#039;;

import { useEffect } from &#039;react&#039;;

const ProductListWithPrefetch = ({ currentPage }: { currentPage: number }) =&gt; {

const queryClient = useQueryClient();

// Prefetch da próxima página quando a página atual carregar

useEffect(() =&gt; {

queryClient.prefetchQuery({

queryKey: [&#039;products&#039;, currentPage + 1],

queryFn: async () =&gt; {

const response = await fetch(/api/products?page=${currentPage + 1});

return response.json();

},

staleTime: 5 60 1000,

});

}, [currentPage, queryClient]);

// Query da página atual

const { data: products } = useQuery({

queryKey: [&#039;products&#039;, currentPage],

queryFn: async () =&gt; {

const response = await fetch(/api/products?page=${currentPage});

return response.json();

},

});

return (

&lt;div&gt;

{/ renderizar produtos /}

&lt;/div&gt;

);

};</code></pre>

<p>Uma implementação mais sofisticada é prefetch em hover. Quando o usuário passa o mouse sobre um link, você já carrega os dados que ele provavelmente vai ver:</p>

<pre><code class="language-typescript">import { useQueryClient } from &#039;@tanstack/react-query&#039;;

const UserLink = ({ userId }: { userId: string }) =&gt; {

const queryClient = useQueryClient();

const handleMouseEnter = () =&gt; {

queryClient.prefetchQuery({

queryKey: [&#039;user&#039;, userId],

queryFn: async () =&gt; {

const response = await fetch(/api/users/${userId});

return response.json();

},

staleTime: 10 60 1000,

});

};

return (

&lt;a

href={/users/${userId}}

onMouseEnter={handleMouseEnter}

&gt;

Ver Perfil

&lt;/a&gt;

);

};</code></pre>

<h2>Optimistic UI: Atualizações Antes da Confirmação do Servidor</h2>

<p>Optimistic UI é quando você atualiza a interface do usuário imediatamente, antes de confirmar com o servidor que a operação foi bem-sucedida. Isso cria uma experiência mais rápida e responsiva. Se algo der errado, você reverte a mudança.</p>

<p>O React Query facilitou isso com as callbacks <code>onMutate</code>, <code>onSuccess</code> e <code>onError</code> do <code>useMutation</code>. A estratégia típica é: quando o usuário submete uma ação, você atualiza o cache otimisticamente, faz a requisição, e se falhar, reverte.</p>

<pre><code class="language-typescript">import { useMutation, useQueryClient } from &#039;@tanstack/react-query&#039;;

interface Post {

id: string;

title: string;

content: string;

}

const EditPost = ({ postId, initialPost }: { postId: string; initialPost: Post }) =&gt; {

const queryClient = useQueryClient();

const editPostMutation = useMutation({

mutationFn: async (updatedPost: Post) =&gt; {

const response = await fetch(/api/posts/${postId}, {

method: &#039;PATCH&#039;,

headers: { &#039;Content-Type&#039;: &#039;application/json&#039; },

body: JSON.stringify(updatedPost),

});

if (!response.ok) {

throw new Error(&#039;Falha ao atualizar post&#039;);

}

return response.json();

},

onMutate: async (newPost) =&gt; {

// Cancela qualquer query em andamento para evitar sobrescrita

await queryClient.cancelQueries({ queryKey: [&#039;post&#039;, postId] });

// Salva os dados antigos em caso de rollback

const previousPost = queryClient.getQueryData&lt;Post&gt;([&#039;post&#039;, postId]);

// Atualiza o cache otimisticamente

queryClient.setQueryData([&#039;post&#039;, postId], newPost);

// Retorna o contexto para usar em onError

return { previousPost };

},

onSuccess: (data) =&gt; {

// Sucesso confirmado - dados já estão corretos no cache

console.log(&#039;Post atualizado com sucesso:&#039;, data);

},

onError: (error, variables, context) =&gt; {

// Se algo der errado, reverte para os dados antigos

if (context?.previousPost) {

queryClient.setQueryData([&#039;post&#039;, postId], context.previousPost);

}

console.error(&#039;Erro ao atualizar post:&#039;, error);

},

});

const handleSave = (updatedPost: Post) =&gt; {

editPostMutation.mutate(updatedPost);

};

return (

&lt;div&gt;

{editPostMutation.isPending &amp;&amp; &lt;span&gt;Salvando...&lt;/span&gt;}

{editPostMutation.isError &amp;&amp; &lt;span&gt;Erro ao salvar&lt;/span&gt;}

{/ Formulário aqui /}

&lt;/div&gt;

);

};</code></pre>

<p>Um exemplo ainda mais realista é quando você está atualizando uma lista e precisa refletir a mudança imediatamente:</p>

<pre><code class="language-typescript">import { useMutation, useQueryClient } from &#039;@tanstack/react-query&#039;;

interface Task {

id: string;

title: string;

completed: boolean;

}

const TaskToggle = ({ task }: { task: Task }) =&gt; {

const queryClient = useQueryClient();

const toggleTaskMutation = useMutation({

mutationFn: async (completed: boolean) =&gt; {

const response = await fetch(/api/tasks/${task.id}, {

method: &#039;PATCH&#039;,

headers: { &#039;Content-Type&#039;: &#039;application/json&#039; },

body: JSON.stringify({ completed }),

});

return response.json();

},

onMutate: async (newCompleted) =&gt; {

// Cancela requisições simultâneas

await queryClient.cancelQueries({ queryKey: [&#039;tasks&#039;] });

// Salva estado anterior

const previousTasks = queryClient.getQueryData&lt;Task[]&gt;([&#039;tasks&#039;]);

// Atualiza cache otimisticamente

queryClient.setQueryData([&#039;tasks&#039;], (oldTasks: Task[]) =&gt;

oldTasks.map((t) =&gt;

t.id === task.id ? { ...t, completed: newCompleted } : t

)

);

return { previousTasks };

},

onError: (error, variables, context) =&gt; {

// Reverte em caso de erro

if (context?.previousTasks) {

queryClient.setQueryData([&#039;tasks&#039;], context.previousTasks);

}

},

});

return (

&lt;input

type=&quot;checkbox&quot;

checked={task.completed}

onChange={(e) =&gt; toggleTaskMutation.mutate(e.target.checked)}

disabled={toggleTaskMutation.isPending}

/&gt;

);

};</code></pre>

<p>A chave do Optimistic UI é sempre ter um plano de reversão. Se você não conseguir reverter facilmente, não use essa técnica, pois pode deixar o usuário com dados inconsistentes.</p>

<h2>Conclusão</h2>

<p>Os três pilares avançados do React Query que dominamos neste artigo — <strong>Cache inteligente com Stale Time</strong>, <strong>Prefetch para antecipar dados</strong>, e <strong>Optimistic UI para feedback imediato</strong> — transformam a experiência do usuário de uma forma que é invisível mas profundamente sentida. Você não apenas reduz requisições desnecessárias, mas cria interfaces que parecem instantâneas mesmo com conexões lentas.</p>

<p>A regra prática que deve guiar suas decisões: configure um <code>staleTime</code> apropriado para quantas vezes seus dados mudam (dados estáticos = staleTime alto), use <code>prefetchQuery()</code> quando puder antecipar onde o usuário vai clicar, e implemente Optimistic UI apenas em ações onde você pode reverter com segurança. O React Query fornece todos os mecanismos; a verdadeira maestria está em saber quando e como usá-los.</p>

<h2>Referências</h2>

<ul>

<li><a href="https://tanstack.com/query/latest" target="_blank" rel="noopener noreferrer">TanStack Query Official Documentation</a></li>

<li><a href="https://tanstack.com/query/latest/docs/react/installation" target="_blank" rel="noopener noreferrer">React Query v4 to v5 Migration Guide</a></li>

<li><a href="https://tanstack.com/query/latest/docs/react/important-defaults" target="_blank" rel="noopener noreferrer">Important Defaults - Query Behavior</a></li>

<li><a href="https://tanstack.com/query/latest/docs/react/guides/important-defaults#stale-time" target="_blank" rel="noopener noreferrer">Optimistic Updates Pattern</a></li>

<li><a href="https://tkdodo.eu/blog/practical-react-query" target="_blank" rel="noopener noreferrer">Tkdodo&#039;s React Query Blog - Practical React Query</a></li>

</ul>

<p>&lt;!-- FIM --&gt;</p>

Comentários

Mais em React & Frontend

Boas Práticas de React Query: Mutations, Invalidações e Sincronização de Cache para Times Ágeis
Boas Práticas de React Query: Mutations, Invalidações e Sincronização de Cache para Times Ágeis

Introdução ao React Query e o Ciclo de Vida das Mutações React Query é uma bi...

Arquitetura de Frontend em Escala: Decisões, Trade-offs e Evolução: Do Básico ao Avançado
Arquitetura de Frontend em Escala: Decisões, Trade-offs e Evolução: Do Básico ao Avançado

Fundamentos de Arquitetura Frontend em Escala A arquitetura de frontend em es...

Dominando React DevTools Avançado: Profiler, Flamegraph e Otimização Guiada em Projetos Reais
Dominando React DevTools Avançado: Profiler, Flamegraph e Otimização Guiada em Projetos Reais

O que é React DevTools e Por Que Importa React DevTools é uma extensão do nav...