<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Ignacio Martin Vazquez</title>
    <description>The latest articles on DEV Community by Ignacio Martin Vazquez (@ignasave).</description>
    <link>https://dev.to/ignasave</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F496801%2F6cc6b91a-35b1-46c5-901e-44baf75d2545.png</url>
      <title>DEV Community: Ignacio Martin Vazquez</title>
      <link>https://dev.to/ignasave</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ignasave"/>
    <language>en</language>
    <item>
      <title>The best pattern for Tanstack Query in a Big App</title>
      <dc:creator>Ignacio Martin Vazquez</dc:creator>
      <pubDate>Sat, 03 Jan 2026 03:27:58 +0000</pubDate>
      <link>https://dev.to/ignasave/we-kept-breaking-cache-invalidation-in-tanstack-query-so-we-stopped-managing-it-manually-47k2</link>
      <guid>https://dev.to/ignasave/we-kept-breaking-cache-invalidation-in-tanstack-query-so-we-stopped-managing-it-manually-47k2</guid>
      <description>&lt;p&gt;TanStack Query is excellent at fetching and caching server state.&lt;br&gt;
But in real applications, teams eventually hit the same wall:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;cache keys become inconsistent&lt;/li&gt;
&lt;li&gt;invalidation logic spreads everywhere&lt;/li&gt;
&lt;li&gt;mutations silently fail to refresh the right data&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The problem isn’t TanStack Query.&lt;/p&gt;

&lt;p&gt;The problem is that cache key management is left entirely up to the application.&lt;/p&gt;

&lt;p&gt;This post shows the pattern we ended up formalizing to fix that — the same one documented in Query Cache Flow.&lt;/p&gt;

&lt;p&gt;The real pain points (from production)&lt;/p&gt;

&lt;p&gt;In larger React apps, we kept running into the same four issues:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Manual cache keys are error-prone&lt;br&gt;
Cache keys are just arrays. Typos and mismatches are easy and invisible.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Invalidation logic is hard to reason about&lt;br&gt;
After a mutation, it’s unclear which queries should be invalidated — lists, details, filters, or all of them.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Generated hooks don’t solve cache consistency&lt;br&gt;
Even with OpenAPI + codegen (e.g. KUBB), cache keys and invalidation are still manual.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;No shared standard across the team&lt;br&gt;
Every developer ends up inventing their own conventions.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These issues don’t show up immediately — they surface weeks later as stale UI bugs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The idea: stop inventing cache keys by hand&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Instead of manually defining cache keys everywhere, we wanted:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a single source of truth for cache keys&lt;/li&gt;
&lt;li&gt;predictable invalidation behavior&lt;/li&gt;
&lt;li&gt;something that integrates naturally with generated hooks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That led to a simple rule:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Cache keys and invalidation should be generated, not handwritten.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;The pipeline (as documented)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Query Cache Flow formalizes a pipeline that already exists in many teams:&lt;/p&gt;

&lt;p&gt;REST API&lt;br&gt;
→ OpenAPI spec&lt;br&gt;
→ KUBB (or other codegen)&lt;br&gt;
→ autogenerated query wrappers&lt;br&gt;
→ zero-thought usage&lt;/p&gt;

&lt;p&gt;Cache behavior becomes part of the contract, not an afterthought.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The core primitive: &lt;code&gt;createQueryGroupCRUD&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;From the docs, everything starts with a query group.&lt;/p&gt;

&lt;p&gt;Example taken directly from the documented approach:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { createQueryGroupCRUD } from '@/queries'

export const accountsQueryGroup =
  createQueryGroupCRUD&amp;lt;string&amp;gt;('accounts')
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This single line defines:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;all query keys related to accounts&lt;/li&gt;
&lt;li&gt;how CRUD operations invalidate related queries&lt;/li&gt;
&lt;li&gt;a consistent structure for lists and details&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No strings. No duplication.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Using it with generated hooks&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Instead of calling generated hooks directly, they’re wrapped once with a stable query key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export const useAccounts = () =&amp;gt;
  generatedUseAccounts({
    query: {
      queryKey: [accountsQueryGroup.list.queryKey],
    },
  })
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key points:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the query key comes from the query group&lt;/li&gt;
&lt;li&gt;every consumer uses the same key structure&lt;/li&gt;
&lt;li&gt;no one invents keys in components anymore&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Mutations and automatic invalidation&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is where the pattern pays off.&lt;/p&gt;

&lt;p&gt;After a mutation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;await createAccount.mutateAsync(data)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Invalidation is not guessed or re-implemented per mutation.&lt;/p&gt;

&lt;p&gt;Instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;invalidateQueriesForKeys([
  accountsQueryGroup.create.invalidates,
])
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That invalidation key already knows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;which lists must refresh&lt;/li&gt;
&lt;li&gt;which related queries are affected&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This produces automatic invalidation cascades without repeating logic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why this works better than ad-hoc invalidation&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Compare this with the typical approach:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;queryClient.invalidateQueries({ queryKey: ['accounts'] })
queryClient.invalidateQueries({ queryKey: ['accounts', id] })

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;keys are duplicated everywhere&lt;/li&gt;
&lt;li&gt;easy to forget one&lt;/li&gt;
&lt;li&gt;hard to review or refactor&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With query groups:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;invalidation intent is explicit&lt;/li&gt;
&lt;li&gt;behavior is centralized&lt;/li&gt;
&lt;li&gt;reviews become trivial&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What this pattern gives you&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Query Cache Flow provides:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Consistent cache key structure&lt;/li&gt;
&lt;li&gt;Automatic cascade invalidation&lt;/li&gt;
&lt;li&gt;Type-safe cache operations&lt;/li&gt;
&lt;li&gt;First-class integration with code-generated hooks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most importantly:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You stop thinking about cache keys entirely.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;When this pattern makes sense&lt;/p&gt;

&lt;p&gt;This approach is ideal if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;you use OpenAPI + codegen&lt;/li&gt;
&lt;li&gt;you have list/detail relationships&lt;/li&gt;
&lt;li&gt;multiple mutations affect the same data&lt;/li&gt;
&lt;li&gt;you care about long-term maintainability&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your app is tiny, you don’t need this.&lt;/p&gt;

&lt;p&gt;If your app grows, you eventually do.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This is a pattern, not magic&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Query Cache Flow doesn’t:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;replace understanding TanStack Query&lt;/li&gt;
&lt;li&gt;hide how invalidation works&lt;/li&gt;
&lt;li&gt;fix poorly designed APIs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What it does is formalize cache behavior so it stops being implicit knowledge.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Final thought&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Cache invalidation is famously hard.&lt;/p&gt;

&lt;p&gt;But most of the pain comes from lack of structure, not from React Query itself.&lt;/p&gt;

&lt;p&gt;Once cache keys and invalidation are treated as first-class, generated artifacts — the problem becomes boring again.&lt;/p&gt;

&lt;p&gt;And boring is exactly what you want here.&lt;/p&gt;

&lt;p&gt;Docs:&lt;br&gt;
👉 &lt;a href="https://querycacheflow.com/docs/intro" rel="noopener noreferrer"&gt;https://querycacheflow.com/docs/intro&lt;/a&gt;&lt;/p&gt;

</description>
      <category>react</category>
      <category>typescript</category>
      <category>api</category>
      <category>tanstack</category>
    </item>
  </channel>
</rss>
