skip to content

In TanStack Query v5, which cached queries does invalidateQueries({ queryKey: ['todos'] }) match, and how do exact: true and predicate narrow that?

level: middleimportance: must knowfreq 60%

answer

  1. array elements compared from the start
  2. objects match as a subset
  3. a flag for the whole key
  4. a function over each Query

basics

~20 s

Without options it matches every query whose key starts with 'todos', element by element, with objects matched by the properties you pass. exact: true matches only the identical key; predicate receives each Query and must also return true.

solid answer

~40 s

Query filters in v5 match keys **by prefix**: `['todos']` matches `['todos']`, `['todos', 'list']` and `['todos', 'detail', 5]`. Each element is compared in turn, and an object element matches when the cached object has all the properties you gave with equal values - `['todos', { status: 'done' }]` also matches `['todos', { status: 'done', page: 2 }]`. Adding `exact: true` compares the whole key instead (property order in objects does not matter, because keys are hashed with sorted object keys). A `predicate` is a function that receives each `Query` and returns a boolean; it is combined with the other filters with AND, so it narrows further, or it can stand alone for rules a prefix cannot express, like a version number in the key. Calling `invalidateQueries()` with no filters matches everything.

go deeper

for a junior

Recall that a queryKey filter matches every key that starts with it, and that exact: true restricts it to the identical key.

for a middle

Explain element-by-element prefix matching, partial matching of object elements, sorted-property hashing, and how predicate combines with the other filters.

for a senior

Pick the narrowest filter for each write so on-screen queries do not refetch needlessly, and use predicate with meta for cross-cutting invalidation.

for a principal

Tie invalidation rules to a shared key hierarchy so that a prefix reliably means one resource across the codebase, and review broad filters as a cost.

## Query filters `invalidateQueries`, `refetchQueries`, `cancelQueries`, `removeQueries`, `resetQueries` and `setQueriesData` all take the same **query filters** object in TanStack Query v5. The fields that select *which* queries are: - **`queryKey`** - a key or key prefix to match; - **`exact`** - match the key exactly instead of by prefix; - **`predicate`** - a function `(query) => boolean` run for each candidate; - **`type`** - `'active'`, `'inactive'` or `'all'` (the filter default is `'all'`); - **`stale`** and **`fetchStatus`** - match by staleness or current fetch state. All provided fields must match - they are combined with **AND**. Leaving everything out matches every query in the cache. ## Prefix matching, element by element With only `queryKey`, a cached query matches when the filter key is a prefix of the cached key: 1. the cached key must be at least as long as the filter key; 2. each filter element is compared with the element at the same position; 3. primitives must be equal; 4. an **object** element matches if every property **in the filter object** matches the same property in the cached object - extra properties in the cached object are ignored. | Filter `queryKey` | Cached key | Match? | |---|---|---| | `['todos']` | `['todos']` | yes | | `['todos']` | `['todos', 'list', { status: 'done' }]` | yes | | `['todos', 'list']` | `['todos', 'detail', 5]` | no - second element differs | | `['todos', { status: 'done' }]` | `['todos', { status: 'done', page: 2 }]` | yes - subset of properties | | `['todos', { status: 'done' }]` | `['todos']` | no - cached key is shorter | | `['todo']` | `['todos']` | no - strings compare whole | The fourth row surprises people: object elements are matched **partially**, so a filter object is itself a kind of prefix. ## `exact: true` `exact: true` switches to comparing the **full key hash**. Only the identical key matches: ```ts queryClient.invalidateQueries({ queryKey: ['todos'], exact: true }) // matches ['todos'] only - not ['todos', 'list'] or ['todos', { page: 1 }] ``` Because keys are hashed with object properties sorted, `{ page: 1, status: 'done' }` and `{ status: 'done', page: 1 }` are the same key; property order never matters. Use `exact` when a parent key and its children coexist and only the parent changed - for example a `['todos']` summary count stored next to `['todos', ...]` lists. ## `predicate` When the rule cannot be written as a prefix, pass a `predicate`. It receives the `Query` object - `query.queryKey`, `query.state`, `query.meta` and more - and returns `true` to include it: ```ts queryClient.invalidateQueries({ predicate: (query) => { const [scope, params] = query.queryKey as readonly [string, { version?: number }?] return scope === 'todos' && (params?.version ?? 0) >= 10 }, }) ``` Two properties are worth stating in an interview: - **it is ANDed** with `queryKey`, `type` and the rest - passing `queryKey: ['todos']` plus a predicate runs the predicate only on keys that already start with `'todos'`; - **it sees the whole query**, so it can select by `meta` tags or by `state` (for example only queries that errored), not just by key. ## Choosing the narrowest filter - **Prefix** for "everything about this resource" - the common case after a create or delete. - **Longer prefix** (`['todos', 'list']`) to spare detail views that the write did not change. - **`exact`** when a parent key and its descendants must be treated differently. - **`predicate`** for cross-cutting rules: a meta tag, a version, a state. Every extra query matched is a network request if it is active. Over-broad filters are not wrong - the data ends up correct - but they cost requests and, on busy screens, visible background refetch indicators. ## Where key design stops and matching starts How well a prefix can target the right queries depends on how the keys were designed in the first place - a hierarchical key with the resource first and the variables last makes prefixes useful. Matching is the mechanism described here; designing the key shape is a separate discipline.

  • Does invalidateQueries({ queryKey: ['todos', { status: 'done' }] }) match ['todos', { status: 'done', page: 3 }]?
    Yes. Object elements match when every property in the filter object equals the one in the cached object; extra cached properties like `page` are ignored. Add `exact: true` if only the key without `page` should match.
  • Can a predicate be combined with queryKey, and in what way?
    Yes, and all filters are ANDed. The `queryKey` prefix narrows the candidates first, and the `predicate` must also return `true`, so it can only narrow further - it cannot add queries outside the prefix.

saying these in an interview costs you the question

  • Believes queryKey filters match only identical keys unless a flag widens them.
  • Thinks object elements in a filter key must match the cached object exactly.
  • Assumes object property order changes whether two keys are equal.
  • Expects a predicate to add queries on top of the queryKey match, as an OR.
  • Calls invalidateQueries with no filters without realising it matches the whole cache.