Query keys
Every builder writes its own key. Knowing the four shapes is what lets you invalidate a whole collection, or exactly one route call, without guessing.
You never write a key by hand, but you do have to invalidate. That means knowing what a key looks like and where to cut it.
The four shapes
Collection read [...prefix, 'collections', name, op, locale, stage, options]
Global read [...prefix, 'globals', name, op, locale, stage, options]
Any write [...prefix, branch, name, op, locale, stage]
Route query [...prefix, 'routes', ...segments, 'query', locale, input]prefix is ['questpie'] unless you changed it. branch is collections or
globals. op is the builder name: find, count, findOne,
findVersions, create, update and so on.
The locale and stage slots are always there, holding undefined when you
did not set them. That is why a prefix match is the safe way to invalidate.
Two keys sit outside those shapes. A route mutation key ends at 'mutation', locale, and routes have no stage slot at all. A channel key is
[...prefix, 'channels', name, 'subscription' | 'presence', args], with
neither slot.
Invalidating
q.key(parts) is the one helper. It puts your prefix on the front and hands
back a QueryKey.
// Every posts query: all operations, all options, all locales.
queryClient.invalidateQueries({ queryKey: q.key(["collections", "posts"]) });
// Only the find queries on that collection.
queryClient.invalidateQueries({
queryKey: q.key(["collections", "posts", "find"]),
});For an exact match, take the key off the builder you already called. Every
query options object carries its own queryKey.
const { queryKey } = q.collections.posts.find({ where: { published: true } });
queryClient.invalidateQueries({ queryKey });Routes have a third way. .key(input) rebuilds the query key without building
the query, which suits prefetching too.
queryClient.invalidateQueries({
queryKey: q.routes.dashboard.getStats.post.key({ period: "week" }),
});`.key()` is a route thing
Collections, globals and channels have no .key() builder. Use top-level
q.key([...]) for a partial match, or read queryKey off the options object
for an exact one.
What gets stripped
Options are sanitized before they land in a key, so a non-serializable value cannot break React Query's structural comparison.
| In your options | In the key |
|---|---|
| a function value | dropped |
an undefined value | dropped |
a null value | kept |
| no options at all | {} |
Two calls differing only by a function share a cache entry
Both keys sanitize to the same thing, so both queries read and write the same entry. Never encode a discriminator as a function inside read options.
Mutation keys
A mutation key stops before the variables. create() on posts is always
['questpie', 'collections', 'posts', 'create', locale, stage], whatever you
pass to mutate(). Mutations do not invalidate anything on their own, so do
it in onSuccess.
const create = useMutation({
...q.collections.posts.create(),
onSuccess: () =>
queryClient.invalidateQueries({
queryKey: q.key(["collections", "posts"]),
}),
});Changing the prefix
keyPrefix moves every key at once. Pass [] and keys start at
'collections' with nothing in front. See
Configuration, which also covers what a
proxy-level locale or stage puts in those two slots.