Skip to content

raf-query is early. The API can still change between minor releases, so use it at your own risk.

raf-query

Guides

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, from the server to the browser, so they must be plain data: strings, numbers, booleans, null, arrays and plain objects.

Groups

Build keys with a group. A group names a set of queries, so one call can invalidate them all. group(name) comes from @raf-query/react, and group(parent, name) makes a group inside another:

export const const posts: Group<readonly ["posts"]>posts = group<"posts">(name: "posts"): Group<readonly ["posts"]> (+1 overload)

A group, or a subgroup inside parent. Call it for a query's key: a flat, plain array that starts with the group's path. Invalidating a group covers its subgroups and every query below them.

group
("posts");
export const const drafts: Group<readonly ["posts", "drafts"]>drafts = group<readonly ["posts"], "drafts">(parent: Group<readonly ["posts"]>, name: "drafts"): Group<readonly ["posts", "drafts"]> (+1 overload)

A group, or a subgroup inside parent. Call it for a query's key: a flat, plain array that starts with the group's path. Invalidating a group covers its subgroups and every query below them.

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

Its parents' names and its own.

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. 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:

export function 
function postQuery(input: {
    slug: string;
}): QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>
postQuery
(
input: {
    slug: string;
}
input
: { slug: stringslug: string }) {
return
queryOptions<readonly ["posts", "bySlug", string], Post, Post>(options: BaseOptions<readonly ["posts", "bySlug", string], DefaultTypes> & {
    readonly queryFn: (context: QueryFunctionContext<readonly ["posts", "bySlug", string]>) => Awaitable<Post>;
    readonly select?: ((data: Post) => Post) | undefined;
}): QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes> (+1 overload)

Typed options for a query factory:

queryOptions
({
BaseOptions<readonly ["posts", "bySlug", string], DefaultTypes>.queryKey: readonly ["posts", "bySlug", string]

A plain array, usually built by a group(). Handles keep its literal type.

queryKey
: ["posts", "bySlug",
input: {
    slug: string;
}
input
.slug: stringslug],
queryFn: (context: QueryFunctionContext<readonly ["posts", "bySlug", string]>) => Awaitable<Post>queryFn: () => function getPost(slug: string): Promise<Post>getPost(
input: {
    slug: string;
}
input
.slug: stringslug),
}); } await function invalidateMany(client: QueryClient, group: Group): Promise<void> (+3 overloads)

Mark queries stale: a factory's (optionally only those whose input matches partial), a group's (keys that start with its path, whether the group built them or not), or those a target's where picks. Read queries refetch now; the rest when next read. Resolves when the refetches settle.

invalidateMany
(
const qc: QueryClient<{
    readonly meta: Flat<unknown>;
}>
qc
, const posts: Group<readonly ["posts"]>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
const const settings: Group<readonly ["settings"]>settings = group<"settings">(name: "settings"): Group<readonly ["settings"]> (+1 overload)

A group, or a subgroup inside parent. Call it for a query's key: a flat, plain array that starts with the group's path. Invalidating a group covers its subgroups and every query below them.

group
("settings");
// A list of posts
infiniteQueryOptions<readonly ["posts", "list"], PostPage, number, InfiniteData<PostPage, number>>(options: InfiniteOptions<readonly ["posts", "list"], PostPage, number, InfiniteData<PostPage, number>, DefaultTypes> & {
    readonly queryFn: (context: InfiniteQueryFunctionContext<readonly ["posts", "list"], number>) => PostPage | PromiseLike<PostPage>;
}): InfiniteQueryOptions<readonly ["posts", "list"], PostPage, number, InfiniteData<...>, DefaultTypes>

Typed options for a paged query factory. Readers get { pages, pageParams } and, on their state, fetchNextPage and fetchPreviousPage:

infiniteQueryOptions
({
BaseOptions<readonly ["posts", "list"], DefaultTypes>.queryKey: readonly ["posts", "list"]

A plain array, usually built by a group(). Handles keep its literal type.

queryKey
:
const posts: Group
<readonly ["list"]>(rest_0: "list") => readonly ["posts", "list"]
posts
("list"),
queryFn: (context: InfiniteQueryFunctionContext<readonly ["posts", "list"], number>) => PostPage | PromiseLike<PostPage>queryFn: (context: InfiniteQueryFunctionContext<readonly ["posts", "list"], number>context) =>
function listPostPage(input: {
    authorId?: string;
    cursor: number;
    drafts?: boolean;
}): Promise<PostPage>
listPostPage
({ cursor: numbercursor: context: InfiniteQueryFunctionContext<readonly ["posts", "list"], number>context.InfiniteQueryFunctionContext<readonly ["posts", "list"], number>.pageParam: numberpageParam }),
InfiniteOptions<readonly ["posts", "list"], PostPage, number, InfiniteData<PostPage, number>, DefaultTypes>.initialPageParam: numberinitialPageParam: 0, InfiniteOptions<readonly ["posts", "list"], PostPage, number, InfiniteData<PostPage, number>, DefaultTypes>.getNextPageParam: PageParamFn<PostPage, number>getNextPageParam: (lastPage: PostPagelastPage) => lastPage: PostPagelastPage.PostPage.nextCursor: number | nullnextCursor, }); // Something else, whatever!
queryOptions<readonly ["settings"], unknown, unknown>(options: BaseOptions<readonly ["settings"], DefaultTypes> & {
    readonly queryFn: (context: QueryFunctionContext<readonly ["settings"]>) => unknown;
    readonly select?: ((data: unknown) => unknown) | undefined;
}): QueryOptions<readonly ["settings"], unknown, unknown, DefaultTypes> (+1 overload)

Typed options for a query factory:

queryOptions
({
BaseOptions<readonly ["settings"], DefaultTypes>.queryKey: readonly ["settings"]

A plain array, usually built by a group(). Handles keep its literal type.

queryKey
:
const settings: Group
<readonly []>() => readonly ["settings"]
settings
(),
queryFn: (context: QueryFunctionContext<readonly ["settings"]>) => unknownqueryFn: () => function getSpecial(): Promise<unknown>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
// An individual post
queryOptions<readonly ["posts", "bySlug", "hello-world"], Post, Post>(options: BaseOptions<readonly ["posts", "bySlug", "hello-world"], DefaultTypes> & {
    readonly queryFn: (context: QueryFunctionContext<readonly ["posts", "bySlug", "hello-world"]>) => Awaitable<Post>;
    readonly select?: ((data: Post) => Post) | undefined;
}): QueryOptions<readonly ["posts", "bySlug", "hello-world"], Post, Post, DefaultTypes> (+1 overload)

Typed options for a query factory:

queryOptions
({
BaseOptions<readonly ["posts", "bySlug", "hello-world"], DefaultTypes>.queryKey: readonly ["posts", "bySlug", "hello-world"]

A plain array, usually built by a group(). Handles keep its literal type.

queryKey
:
const posts: Group
<readonly ["bySlug", "hello-world"]>(rest_0: "bySlug", rest_1: "hello-world") => readonly ["posts", "bySlug", "hello-world"]
posts
("bySlug", "hello-world"),
queryFn: (context: QueryFunctionContext<readonly ["posts", "bySlug", "hello-world"]>) => Awaitable<Post>queryFn: () => function getPost(slug: string): Promise<Post>getPost("hello-world"), }); // An individual post in a "preview" format
queryOptions<readonly ["posts", "bySlug", "hello-world", {
    readonly preview: true;
}], unknown, unknown>(options: BaseOptions<readonly ["posts", "bySlug", "hello-world", {
    readonly preview: true;
}], DefaultTypes> & {
    readonly queryFn: (context: QueryFunctionContext<readonly ["posts", "bySlug", "hello-world", {
        readonly preview: true;
    }]>) => unknown;
    readonly select?: ((data: unknown) => unknown) | undefined;
}): QueryOptions<readonly ["posts", "bySlug", "hello-world", {
    readonly preview: true;
}], unknown, unknown, DefaultTypes> (+1 overload)

Typed options for a query factory:

queryOptions
({
BaseOptions<readonly ["posts", "bySlug", "hello-world", { readonly preview: true; }], DefaultTypes>.queryKey: readonly ["posts", "bySlug", "hello-world", {
    readonly preview: true;
}]

A plain array, usually built by a group(). Handles keep its literal type.

queryKey
:
const posts: Group
<readonly ["bySlug", "hello-world", {
    readonly preview: true;
}]>(rest_0: "bySlug", rest_1: "hello-world", rest_2: {
    readonly preview: true;
}) => readonly ["posts", "bySlug", "hello-world", {
    readonly preview: true;
}]
posts
("bySlug", "hello-world", { preview: truepreview: true }),
queryFn: (context: QueryFunctionContext<readonly ["posts", "bySlug", "hello-world", {
    readonly preview: true;
}]>) => unknown
queryFn
,
}); // A list of one author's drafts
infiniteQueryOptions<readonly ["posts", "drafts", "list", {
    readonly authorId: "ada";
}], PostPage, number, InfiniteData<PostPage, number>>(options: InfiniteOptions<readonly ["posts", "drafts", "list", {
    readonly authorId: "ada";
}], PostPage, number, InfiniteData<PostPage, number>, DefaultTypes> & {
    readonly queryFn: (context: InfiniteQueryFunctionContext<readonly ["posts", "drafts", "list", {
        readonly authorId: "ada";
    }], number>) => PostPage | PromiseLike<...>;
}): InfiniteQueryOptions<...>

Typed options for a paged query factory. Readers get { pages, pageParams } and, on their state, fetchNextPage and fetchPreviousPage:

infiniteQueryOptions
({
BaseOptions<readonly ["posts", "drafts", "list", { readonly authorId: "ada"; }], DefaultTypes>.queryKey: readonly ["posts", "drafts", "list", {
    readonly authorId: "ada";
}]

A plain array, usually built by a group(). Handles keep its literal type.

queryKey
:
const drafts: Group
<readonly ["list", {
    readonly authorId: "ada";
}]>(rest_0: "list", rest_1: {
    readonly authorId: "ada";
}) => readonly ["posts", "drafts", "list", {
    readonly authorId: "ada";
}]
drafts
("list", { authorId: "ada"authorId: "ada" }),
queryFn: (context: InfiniteQueryFunctionContext<readonly ["posts", "drafts", "list", {
    readonly authorId: "ada";
}], number>) => PostPage | PromiseLike<PostPage>
queryFn
: (
context: InfiniteQueryFunctionContext<readonly ["posts", "drafts", "list", {
    readonly authorId: "ada";
}], number>
context
) =>
function listPostPage(input: {
    authorId?: string;
    cursor: number;
    drafts?: boolean;
}): Promise<PostPage>
listPostPage
({ authorId?: string | undefinedauthorId: "ada", cursor: numbercursor:
context: InfiniteQueryFunctionContext<readonly ["posts", "drafts", "list", {
    readonly authorId: "ada";
}], number>
context
.InfiniteQueryFunctionContext<readonly ["posts", "drafts", "list", { readonly authorId: "ada"; }], number>.pageParam: numberpageParam, drafts?: boolean | undefineddrafts: true }),
InfiniteOptions<readonly ["posts", "drafts", "list", { readonly authorId: "ada"; }], PostPage, number, InfiniteData<PostPage, number>, DefaultTypes>.initialPageParam: numberinitialPageParam: 0, InfiniteOptions<readonly ["posts", "drafts", "list", { readonly authorId: "ada"; }], PostPage, number, InfiniteData<PostPage, number>, DefaultTypes>.getNextPageParam: PageParamFn<PostPage, number>getNextPageParam: (lastPage: PostPagelastPage) => lastPage: PostPagelastPage.PostPage.nextCursor: number | nullnextCursor, });

Keys are hashed deterministically

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

const posts: Group
<readonly ["list", {
    readonly sort: string;
    readonly page: number;
}]>(rest_0: "list", rest_1: {
    readonly sort: string;
    readonly page: number;
}) => readonly ["posts", "list", {
    readonly sort: string;
    readonly page: number;
}]
posts
("list", { sort: stringsort, page: numberpage });
const posts: Group
<readonly ["list", {
    readonly page: number;
    readonly sort: string;
}]>(rest_0: "list", rest_1: {
    readonly page: number;
    readonly sort: string;
}) => readonly ["posts", "list", {
    readonly page: number;
    readonly sort: string;
}]
posts
("list", { page: numberpage, sort: stringsort });
const posts: Group
<readonly ["list", {
    readonly page: number;
    readonly sort: string;
    readonly other: undefined;
}]>(rest_0: "list", rest_1: {
    readonly page: number;
    readonly sort: string;
    readonly other: undefined;
}) => readonly ["posts", "list", {
    readonly page: number;
    readonly sort: string;
    readonly other: undefined;
}]
posts
("list", { page: numberpage, sort: stringsort, other: undefinedother: var undefinedundefined });

Array order does matter. These are three different entries:

const posts: Group
<readonly ["list", string, number]>(rest_0: "list", rest_1: string, rest_2: number) => readonly ["posts", "list", string, number]
posts
("list", const sort: stringsort, const page: numberpage);
const posts: Group
<readonly ["list", number, string]>(rest_0: "list", rest_1: number, rest_2: string) => readonly ["posts", "list", number, string]
posts
("list", const page: numberpage, const sort: stringsort);
const posts: Group
<readonly ["list", undefined, number, string]>(rest_0: "list", rest_1: undefined, rest_2: number, rest_3: string) => readonly ["posts", "list", undefined, number, string]
posts
("list", var undefinedundefined, const page: numberpage, const sort: stringsort);

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:

export function 
function postListQuery(input: {
    sort: "newest" | "oldest";
    page: number;
}): QueryOptions<readonly ["posts", "list", {
    sort: "newest" | "oldest";
    page: number;
}], readonly Post[], readonly Post[], DefaultTypes>
postListQuery
(
input: {
    sort: "newest" | "oldest";
    page: number;
}
input
: {
sort: "newest" | "oldest"sort: "newest" | "oldest"; page: numberpage: number; }) { return
queryOptions<readonly ["posts", "list", {
    sort: "newest" | "oldest";
    page: number;
}], readonly Post[], readonly Post[]>(options: BaseOptions<readonly ["posts", "list", {
    sort: "newest" | "oldest";
    page: number;
}], DefaultTypes> & {
    readonly queryFn: (context: QueryFunctionContext<readonly ["posts", "list", {
        sort: "newest" | "oldest";
        page: number;
    }]>) => Awaitable<readonly Post[]>;
    readonly select?: ((data: readonly Post[]) => readonly Post[]) | undefined;
}): QueryOptions<readonly ["posts", "list", {
    sort: "newest" | "oldest";
    page: number;
}], readonly Post[], readonly Post[], DefaultTypes> (+1 overload)

Typed options for a query factory:

queryOptions
({
BaseOptions<readonly ["posts", "list", { sort: "newest" | "oldest"; page: number; }], DefaultTypes>.queryKey: readonly ["posts", "list", {
    sort: "newest" | "oldest";
    page: number;
}]

A plain array, usually built by a group(). Handles keep its literal type.

queryKey
:
const posts: Group
<readonly ["list", {
    sort: "newest" | "oldest";
    page: number;
}]>(rest_0: "list", rest_1: {
    sort: "newest" | "oldest";
    page: number;
}) => readonly ["posts", "list", {
    sort: "newest" | "oldest";
    page: number;
}]
posts
("list",
input: {
    sort: "newest" | "oldest";
    page: number;
}
input
),
queryFn: (context: QueryFunctionContext<readonly ["posts", "list", {
    sort: "newest" | "oldest";
    page: number;
}]>) => Awaitable<readonly Post[]>
queryFn
: () =>
function listPosts(input: {
    page: number;
    sort: "newest" | "oldest";
}): Promise<readonly Post[]>
listPosts
(
input: {
    sort: "newest" | "oldest";
    page: number;
}
input
),
}); }

Each distinct input gets its own entry, cached independently. Because the input comes from the URL (see URL state and pagination), 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:

await function invalidateMany(client: QueryClient, group: Group): Promise<void> (+3 overloads)

Mark queries stale: a factory's (optionally only those whose input matches partial), a group's (keys that start with its path, whether the group built them or not), or those a target's where picks. Read queries refetch now; the rest when next read. Resolves when the refetches settle.

invalidateMany
(
const qc: QueryClient<{
    readonly meta: Flat<unknown>;
}>
qc
, const posts: Group<readonly ["posts"]>posts); // every post query, drafts too
await function invalidateMany(client: QueryClient, group: Group): Promise<void> (+3 overloads)

Mark queries stale: a factory's (optionally only those whose input matches partial), a group's (keys that start with its path, whether the group built them or not), or those a target's where picks. Read queries refetch now; the rest when next read. Resolves when the refetches settle.

invalidateMany
(
const qc: QueryClient<{
    readonly meta: Flat<unknown>;
}>
qc
, const drafts: Group<readonly ["posts", "drafts"]>drafts); // only drafts
await
invalidateMany<(input: {
    sort: "newest" | "oldest";
    page: number;
}) => QueryOptions<readonly ["posts", "list", {
    sort: "newest" | "oldest";
    page: number;
}], readonly Post[], readonly Post[], DefaultTypes>>(client: ClientFor<(input: {
    sort: "newest" | "oldest";
    page: number;
}) => QueryOptions<readonly ["posts", "list", {
    sort: "newest" | "oldest";
    page: number;
}], readonly Post[], readonly Post[], DefaultTypes>>, factory: (input: {
    sort: "newest" | "oldest";
    page: number;
}) => QueryOptions<readonly ["posts", "list", {
    sort: "newest" | "oldest";
    page: number;
}], readonly Post[], readonly Post[], DefaultTypes>, partial?: {
    ...;
} | undefined): Promise<void> (+3 overloads)

Mark queries stale: a factory's (optionally only those whose input matches partial), a group's (keys that start with its path, whether the group built them or not), or those a target's where picks. Read queries refetch now; the rest when next read. Resolves when the refetches settle.

invalidateMany
(
const qc: QueryClient<{
    readonly meta: Flat<unknown>;
}>
qc
,
function postListQuery(input: {
    sort: "newest" | "oldest";
    page: number;
}): QueryOptions<readonly ["posts", "list", {
    sort: "newest" | "oldest";
    page: number;
}], readonly Post[], readonly Post[], DefaultTypes>
postListQuery
); // every page of every list
await
invalidateMany<(input: {
    sort: "newest" | "oldest";
    page: number;
}) => QueryOptions<readonly ["posts", "list", {
    sort: "newest" | "oldest";
    page: number;
}], readonly Post[], readonly Post[], DefaultTypes>>(client: ClientFor<(input: {
    sort: "newest" | "oldest";
    page: number;
}) => QueryOptions<readonly ["posts", "list", {
    sort: "newest" | "oldest";
    page: number;
}], readonly Post[], readonly Post[], DefaultTypes>>, factory: (input: {
    sort: "newest" | "oldest";
    page: number;
}) => QueryOptions<readonly ["posts", "list", {
    sort: "newest" | "oldest";
    page: number;
}], readonly Post[], readonly Post[], DefaultTypes>, partial?: {
    ...;
} | undefined): Promise<void> (+3 overloads)

Mark queries stale: a factory's (optionally only those whose input matches partial), a group's (keys that start with its path, whether the group built them or not), or those a target's where picks. Read queries refetch now; the rest when next read. Resolves when the refetches settle.

invalidateMany
(
const qc: QueryClient<{
    readonly meta: Flat<unknown>;
}>
qc
,
function postListQuery(input: {
    sort: "newest" | "oldest";
    page: number;
}): QueryOptions<readonly ["posts", "list", {
    sort: "newest" | "oldest";
    page: number;
}], readonly Post[], readonly Post[], DefaultTypes>
postListQuery
, { sort?: "newest" | "oldest" | undefinedsort: "newest" }); // the newest-first lists

For sets of queries that cut across groups, such as every query that shows one author, give them tags: a where target over meta, written in your app. See Query invalidation for every target.