# Query keys

> How query keys identify cache entries, how groups name sets of queries, and how to include a factory's input in its key.

At its core, raf-query caches by **query key**. A key is an array at the top level. It can be as simple as one string, or as complex as many strings and nested objects. As long as it serializes with `JSON.stringify` and is **unique to the query's data**, you can use it.

Keys also travel inside [handles](https://raf-query.katt.dev/docs/handles.md), from the server to the browser, so they must be plain data: strings, numbers, booleans, `null`, arrays and plain objects.

> [!NOTE]
> Groups and the way verbs match keys are a design still being settled. The shapes on this page can change before 1.0.

## Groups

Build keys with a **group**. A group names a set of queries, so one call can [invalidate](https://raf-query.katt.dev/docs/query-invalidation.md) them all. `group(name)` comes from `@raf-query/react`, and `group(parent, name)` makes a group inside another:

```ts
export const posts = group("posts");
export const drafts = group(posts, "drafts");

const key = drafts("list");
// const key: readonly ["posts", "drafts", "list"]
drafts.path;
// (property) Group<readonly ["posts", "drafts"]>.path: readonly ["posts", "drafts"]
```

Call a group for a key. The key is a flat, plain array that starts with the group's path, `["posts", "drafts", "list"]`, so it looks the same in memory, on the wire and in [devtools](https://raf-query.katt.dev/docs/devtools.md). Invalidating a group covers every key that starts with its path: `posts` covers `drafts` and every query in it, and `drafts` doesn't cover `posts`.

Verbs take the group itself, never a string or an array, so a typo in a group's name is a type error instead of a target that silently matches nothing.

Two things under one group can't share a name: if `posts("drafts", id)` were a query next to the `drafts` group, invalidating `drafts` would reach it. In development, whichever comes second throws. Production builds leave the check out.

### Hand-written keys

`queryOptions` takes any plain array as a key, so a key doesn't have to come from a group. A group still covers a hand-written key that starts with its path:

```ts
export function postQuery(input: { slug: string }) {
	return queryOptions({
		queryKey: ["posts", "bySlug", input.slug],
		queryFn: () => getPost(input.slug),
	});
}

await invalidateMany(qc, posts); // covers it: its key starts with ["posts"]
```

Prefer groups: a hand-written key repeats the group's names by hand, and the development check for shared names only sees keys a group built.

## Simple keys

The simplest key is a group with a constant, or the group alone. It suits:

- generic list or index resources
- non-hierarchical resources

```ts
const settings = group("settings");

// A list of posts
infiniteQueryOptions({
	queryKey: posts("list"),
	queryFn: (context) => listPostPage({ cursor: context.pageParam }),
	initialPageParam: 0,
	getNextPageParam: (lastPage) => lastPage.nextCursor,
});

// Something else, whatever!
queryOptions({
	queryKey: settings(),
	queryFn: () => getSpecial(),
});
```

## Keys with variables

When a query needs more information to describe its data, pass it to the group after the query's name: an id, an index, or an object of parameters. This suits:

- hierarchical or nested resources, usually identified by an id
- queries with parameters, usually passed as an object

```ts
// An individual post
queryOptions({
	queryKey: posts("bySlug", "hello-world"),
	queryFn: () => getPost("hello-world"),
});

// An individual post in a "preview" format
queryOptions({
	queryKey: posts("bySlug", "hello-world", { preview: true }),
	queryFn,
});

// A list of one author's drafts
infiniteQueryOptions({
	queryKey: drafts("list", { authorId: "ada" }),
	queryFn: (context) =>
		listPostPage({ authorId: "ada", cursor: context.pageParam, drafts: true }),
	initialPageParam: 0,
	getNextPageParam: (lastPage) => lastPage.nextCursor,
});
```

## Keys are hashed deterministically

Object keys are sorted before hashing, and `undefined` fields are dropped, so all of these name the same entry:

```ts
posts("list", { sort, page });
posts("list", { page, sort });
posts("list", { page, sort, other: undefined });
```

Array order does matter. These are three different entries:

```ts
posts("list", sort, page);
posts("list", page, sort);
posts("list", undefined, page, sort);
```

`useQuery`'s state exposes the hashed key as `hash`, which makes a good `key` prop for a Suspense boundary.

## Put the input in the key

A query's key must change whenever its data would. In raf-query that's natural: a factory's single argument is its **input**, and the key is built from it. Make the input the **last** segment:

```ts
export function postListQuery(input: {
	sort: "newest" | "oldest";
	page: number;
}) {
	return queryOptions({
		queryKey: posts("list", input),
		queryFn: () => listPosts(input),
	});
}
```

Each distinct input gets its own entry, cached independently. Because the input comes from the URL (see [URL state and pagination](https://raf-query.katt.dev/docs/url-state-and-pagination.md)), a new search param means a new key, and the loader starts it.

## Design keys for invalidation

A group matches every key that starts with its path. A factory matches the queries it built, and a factory with part of its input matches the queries whose input has those fields. Every level is one call:

```ts
await invalidateMany(qc, posts); // every post query, drafts too
await invalidateMany(qc, drafts); // only drafts
await invalidateMany(qc, postListQuery); // every page of every list
await invalidateMany(qc, postListQuery, { sort: "newest" }); // the newest-first lists
```

For sets of queries that cut across groups, such as every query that shows one author, give them [tags](https://raf-query.katt.dev/docs/query-invalidation.md#tags): a `where` target over `meta`, written in your app. See [Query invalidation](https://raf-query.katt.dev/docs/query-invalidation.md) for every target.
