Key each query by every input it uses
Put every value the query function reads into the query key, as plain serializable values. The key must change whenever the request would change.
Implementation
- Include each ID, filter, page, search term, and option the query function uses.
- Use an array whose parts are strings, numbers, booleans,
null, plain objects, or arrays of these. TanStack Query hashes keys withJSON.stringify, sorting plain object properties, so{ status, page }and{ page, status }match. - Convert other values to a stable, meaningful form before putting them in a key:
- Dates: an ISO string or date string, such as
'2024-01-15'. MapandSet: an array or plain object; both serialize to{}, so different values collide.- Class instances: a plain object of the fields that matter; the class itself is lost.
- Functions and symbols: never in a key; they serialize to
nullor disappear.
- Dates: an ISO string or date string, such as
- Compute key parts from inputs that are stable across renders;
new Date()in a key creates a new cache entry on every render. - Keep the key and the query function together, such as in a
queryOptionsfactory, so they cannot drift apart.
Rationale
TanStack Query identifies cache entries by the hashed key.
If the query function reads a value that is not in the key, two different requests share one entry: switching users shows the previous user's data, and changing a filter does not refetch.
Values that do not serialize meaningfully either collide, as Map and Set do, or produce a new key every time.
Examples
Application: A missing input
Incorrect (counterexample):
function UserPosts({ userId }: { userId: string }) {
const { data } = useQuery({
queryKey: ['posts'],
queryFn: () => fetchPostsByUser(userId),
});
// ...
}
All users share the ['posts'] entry, so navigating from one user to another shows the first user's posts until a refetch completes.
Correct:
function UserPosts({ userId }: { userId: string }) {
const { data } = useQuery({
queryKey: ['posts', { userId }],
queryFn: () => fetchPostsByUser(userId),
});
// ...
}
Application: A value that does not serialize meaningfully
Incorrect (counterexample):
const { data } = useQuery({
queryKey: ['events', selectedTags],
queryFn: () => fetchEvents([...selectedTags]),
});
If selectedTags is a Set, every selection serializes to {}, so all selections share one cache entry.
Correct:
const tags = [...selectedTags].sort();
const { data } = useQuery({
queryKey: ['events', { tags }],
queryFn: () => fetchEvents(tags),
});
Validation
For each query, list the variables the query function reads and check that each appears in the key.
Check that JSON.stringify(queryKey) produces a distinct, stable string for each distinct request.
The TanStack Query ESLint plugin's exhaustive-deps rule can catch missing variables.
A value that does not affect the request, such as a callback used only for logging, does not belong in the key.