We kept breaking cache invalidation in TanStack Query — so we stopped managing it manually

Published: (January 2, 2026 at 10:27 PM EST)
2 min read
Source: Dev.to

Source: Dev.to

The real pain points (from production)

  • Manual cache keys are error‑prone
  • Invalidation logic is hard to reason about
  • Generated hooks don’t solve cache consistency
  • No shared standard across the team

These issues often surface weeks later as stale UI bugs.

The idea: stop inventing cache keys by hand

Instead of defining cache keys everywhere, we wanted:

  • A single source of truth for cache keys
  • Predictable invalidation behavior
  • Seamless integration with generated hooks

Rule: Cache keys and invalidation should be generated, not handwritten.

The pipeline (as documented)

Query Cache Flow formalizes a pipeline that many teams already use:

  1. REST API – Cache behavior becomes part of the contract, not an afterthought.
  2. Core primitive: createQueryGroupCRUD – everything starts with a query group.

The core primitive: createQueryGroupCRUD

import { createQueryGroupCRUD } from '@/queries'

export const accountsQueryGroup = createQueryGroupCRUD('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 string literals, no duplication.

Using it with generated hooks

Wrap generated hooks once with a stable query key:

export const useAccounts = () =>
  generatedUseAccounts({
    query: {
      queryKey: [accountsQueryGroup.list.queryKey],
    },
  })
  • The query key comes from the query group.
  • Every consumer uses the same key structure.
  • No component invents its own keys.

Mutations and automatic invalidation

After a mutation:

await createAccount.mutateAsync(data)

Invalidate queries using the group’s predefined keys:

invalidateQueriesForKeys([
  accountsQueryGroup.create.invalidates,
])

The invalidation key already knows which lists and related queries must refresh, producing automatic cascades without repeating logic.

Why this works better than ad‑hoc invalidation

Typical ad‑hoc approach:

queryClient.invalidateQueries({ queryKey: ['accounts'] })
queryClient.invalidateQueries({ queryKey: ['accounts', id] })

Problems:

  • Keys are duplicated everywhere.
  • Easy to forget a key.
  • Hard to review or refactor.

With query groups:

  • Invalidation intent is explicit.
  • Behavior is centralized.
  • Reviews become trivial.

What this pattern gives you

  • 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

Ideal for:

  • Projects using OpenAPI + codegen
  • List/detail relationships
  • Multiple mutations affecting the same data
  • Long‑term maintainability

If the app is tiny, the overhead may not be justified, but as it grows, the pattern pays off.

This is a pattern, not magic

Query Cache Flow does not:

  • Replace understanding of TanStack Query
  • Hide how invalidation works
  • Fix poorly designed APIs

It formalizes cache behavior so it stops being implicit knowledge.

Final thought

Cache invalidation is famously hard, but most pain comes from lack of structure, not from React Query itself. Treating cache keys and invalidation as first‑class, generated artifacts makes the problem boring—and boring is exactly what you want.

Docs:

Back to Blog

Related posts

Read more »