# Query keys (/docs/client/tanstack-query/keys)

---
title: Query keys
description: 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.
kind: guide
package: "@questpie/tanstack-query"
---

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

```text
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`.

```ts
// 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`.

```ts
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.

```ts
queryClient.invalidateQueries({
	queryKey: q.routes.dashboard.getStats.post.key({ period: "week" }),
});
```

<Callout type="info" title="`.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.
</Callout>

## 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    | `{}`       |

<Callout
	type="warn"
	title="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.
</Callout>

## 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`.

```ts
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](/docs/client/tanstack-query/config), which also covers what a
proxy-level `locale` or `stage` puts in those two slots.
