TanStack Query is excellent at fetching and caching server state.
But in real applications, teams eventually hit the same wall:
- cache keys become inconsistent
- invalidation logic spreads everywhere
- mutations silently fail to refresh the right data
The problem isn’t TanStack Query.
The problem is that cache key management is left entirely up to the application.
This post shows the pattern we ended up formalizing to fix that — the same one documented in Query Cache Flow.
The real pain points (from production)
In larger React apps, we kept running into the same four issues:
Manual cache keys are error-prone
Cache keys are just arrays. Typos and mismatches are easy and invisible.Invalidation logic is hard to reason about
After a mutation, it’s unclear which queries should be invalidated — lists, details, filters, or all of them.Generated hooks don’t solve cache consistency
Even with OpenAPI + codegen (e.g. KUBB), cache keys and invalidation are still manual.No shared standard across the team
Every developer ends up inventing their own conventions.
These issues don’t show up immediately — they surface weeks later as stale UI bugs.
The idea: stop inventing cache keys by hand
Instead of manually defining cache keys everywhere, we wanted:
- a single source of truth for cache keys
- predictable invalidation behavior
- something that integrates naturally with generated hooks
That led to a simple rule:
Cache keys and invalidation should be generated, not handwritten.
The pipeline (as documented)
Query Cache Flow formalizes a pipeline that already exists in many teams:
REST API
→ OpenAPI spec
→ KUBB (or other codegen)
→ autogenerated query wrappers
→ zero-thought usage
Cache behavior becomes part of the contract, not an afterthought.
The core primitive: createQueryGroupCRUD
From the docs, everything starts with a query group.
Example taken directly from the documented approach:
import { createQueryGroupCRUD } from '@/queries'
export const accountsQueryGroup =
createQueryGroupCRUD<string>('accounts')
This single line defines:
- all query keys related to accounts
- how CRUD operations invalidate related queries
- a consistent structure for lists and details
No strings. No duplication.
Using it with generated hooks
Instead of calling generated hooks directly, they’re wrapped once with a stable query key:
export const useAccounts = () =>
generatedUseAccounts({
query: {
queryKey: [accountsQueryGroup.list.queryKey],
},
})
Key points:
- the query key comes from the query group
- every consumer uses the same key structure
- no one invents keys in components anymore
Mutations and automatic invalidation
This is where the pattern pays off.
After a mutation:
await createAccount.mutateAsync(data)
Invalidation is not guessed or re-implemented per mutation.
Instead:
invalidateQueriesForKeys([
accountsQueryGroup.create.invalidates,
])
That invalidation key already knows:
- which lists must refresh
- which related queries are affected
This produces automatic invalidation cascades without repeating logic.
Why this works better than ad-hoc invalidation
Compare this with the typical approach:
queryClient.invalidateQueries({ queryKey: ['accounts'] })
queryClient.invalidateQueries({ queryKey: ['accounts', id] })
Problems:
- keys are duplicated everywhere
- easy to forget one
- hard to review or refactor
With query groups:
- invalidation intent is explicit
- behavior is centralized
- reviews become trivial
What this pattern gives you
Query Cache Flow provides:
- Consistent cache key structure
- Automatic cascade invalidation
- Type-safe cache operations
- First-class integration with code-generated hooks
Most importantly:
You stop thinking about cache keys entirely.
When this pattern makes sense
This approach is ideal if:
- you use OpenAPI + codegen
- you have list/detail relationships
- multiple mutations affect the same data
- you care about long-term maintainability
If your app is tiny, you don’t need this.
If your app grows, you eventually do.
This is a pattern, not magic
Query Cache Flow doesn’t:
- replace understanding TanStack Query
- hide how invalidation works
- fix poorly designed APIs
What it does is formalize cache behavior so it stops being implicit knowledge.
Final thought
Cache invalidation is famously hard.
But most of the pain comes from lack of structure, not from React Query itself.
Once cache keys and invalidation are treated as first-class, generated artifacts — the problem becomes boring again.
And boring is exactly what you want here.
Top comments (0)