Guides
Query keys
How query keys identify cache entries, how they're hashed, 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.
Simple keys
The simplest key is an array of constants. It suits:
- generic list or index resources
- non-hierarchical resources
// A list of todos
infiniteQueryOptions<{
items: readonly Todo[];
nextCursor: null;
}, number, unknown, never, readonly ["todos"], undefined>(options: Omit<InfiniteQueryOptions<{
error: unknown;
key: readonly ["todos"];
page: {
items: readonly Todo[];
nextCursor: null;
};
pageParam: number;
services: never;
}>, "kind" | "throwOnError"> & ThrowOnErrorField<undefined>): InfiniteQueryOptions<Flat<{
key: readonly ["todos"];
page: {
items: readonly Todo[];
nextCursor: null;
};
pageParam: number;
}>>
infiniteQueryOptions({
queryKey: readonly ["todos"]queryKey: ["todos"],
function queryFn(context: PageFunctionContext<number>): QueryFnResult<{
data: {
items: readonly Todo[];
nextCursor: null;
};
error: unknown;
services: never;
}>
queryFn: async () => ({
items: readonly Todo[]items: await function listTodos(input?: {
page?: number;
status?: "open" | "done";
}): Promise<readonly Todo[]>
listTodos(),
nextCursor: nullnextCursor: null,
}),
initialPageParam: numberinitialPageParam: 0,
function getNextPageParam(lastPage: {
items: readonly Todo[];
nextCursor: null;
}, pages: readonly {
items: readonly Todo[];
nextCursor: null;
}[], lastPageParam: number, pageParams: readonly number[]): number | null | undefined
getNextPageParam: (lastPage: {
items: readonly Todo[];
nextCursor: null;
}
lastPage) => lastPage: {
items: readonly Todo[];
nextCursor: null;
}
lastPage.nextCursor: nullnextCursor,
});
// Something else, whatever!
queryOptions<unknown, unknown, never, readonly ["something", "special"], undefined>(options: Omit<QueryOptions<{
data: unknown;
error: unknown;
key: readonly ["something", "special"];
services: never;
}>, "kind" | "throwOnError"> & ThrowOnErrorField<undefined>): QueryOptions<Flat<{
data: unknown;
key: readonly ["something", "special"];
}>>
queryOptions({
queryKey: readonly ["something", "special"]queryKey: ["something", "special"],
function queryFn(context: QueryFunctionContext): unknownqueryFn: () => function getSpecial(): Promise<unknown>getSpecial(),
});
Keys with variables
When a query needs more information to describe its data, add it to the array: 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 todo
queryOptions<Todo, unknown, never, readonly ["todo", 5], undefined>(options: Omit<QueryOptions<{
data: Todo;
error: unknown;
key: readonly ["todo", 5];
services: never;
}>, "kind" | "throwOnError"> & ThrowOnErrorField<undefined>): QueryOptions<Flat<{
data: Todo;
key: readonly ["todo", 5];
}>>
queryOptions({ queryKey: readonly ["todo", 5]queryKey: ["todo", 5], function queryFn(context: QueryFunctionContext): QueryFnResult<{
data: Todo;
error: unknown;
key: readonly ["todo", 5];
services: never;
}>
queryFn: () => function getTodo(id: number): Promise<Todo>getTodo(5) });
// An individual todo in a "preview" format
queryOptions<unknown, unknown, never, readonly ["todo", 5, {
readonly preview: true;
}], undefined>(options: Omit<QueryOptions<{
data: unknown;
error: unknown;
key: readonly ["todo", 5, {
readonly preview: true;
}];
services: never;
}>, "kind" | "throwOnError"> & ThrowOnErrorField<undefined>): QueryOptions<Flat<{
data: unknown;
key: readonly ["todo", 5, {
readonly preview: true;
}];
}>>
queryOptions({ queryKey: readonly ["todo", 5, {
readonly preview: true;
}]
queryKey: ["todo", 5, { preview: truepreview: true }], function queryFn(context: QueryFunctionContext): unknownqueryFn });
// A list of todos that are "done"
infiniteQueryOptions<{
items: unknown;
nextCursor: null;
}, number, unknown, never, readonly ["todos", {
readonly status: "done";
}], undefined>(options: Omit<InfiniteQueryOptions<{
error: unknown;
key: readonly ["todos", {
readonly status: "done";
}];
page: {
items: unknown;
nextCursor: null;
};
pageParam: number;
services: never;
}>, "kind" | "throwOnError"> & ThrowOnErrorField<undefined>): InfiniteQueryOptions<Flat<{
key: readonly ["todos", {
readonly status: "done";
}];
page: {
items: unknown;
nextCursor: null;
};
pageParam: number;
}>>
infiniteQueryOptions({
queryKey: readonly ["todos", {
readonly status: "done";
}]
queryKey: ["todos", { status: "done"status: "done" }],
function queryFn(context: PageFunctionContext<number>): QueryFnResult<{
data: {
items: unknown;
nextCursor: null;
};
error: unknown;
services: never;
}>
queryFn: async () => ({
items: unknownitems: await function queryFn(): Promise<unknown>queryFn(),
nextCursor: nullnextCursor: null,
}),
initialPageParam: numberinitialPageParam: 0,
function getNextPageParam(lastPage: {
items: unknown;
nextCursor: null;
}, pages: readonly {
items: unknown;
nextCursor: null;
}[], lastPageParam: number, pageParams: readonly number[]): number | null | undefined
getNextPageParam: (lastPage: {
items: unknown;
nextCursor: null;
}
lastPage) => lastPage: {
items: unknown;
nextCursor: null;
}
lastPage.nextCursor: nullnextCursor,
});
Keys are hashed deterministically
Object keys are sorted before hashing, and undefined fields are dropped, so all of these name the same entry:
["todos", { status: stringstatus, page: numberpage }];
["todos", { page: numberpage, status: stringstatus }];
["todos", { page: numberpage, status: stringstatus, other: undefinedother: var undefinedundefined }];
Array order does matter. These are three different entries:
["todos", const status: stringstatus, const page: numberpage];
["todos", const page: numberpage, const status: stringstatus];
["todos", var undefinedundefined, const page: numberpage, const status: stringstatus];
hashKey(key) returns the string form, and useQuery's controls expose it 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.
export function function todosQuery(input: {
status: "open" | "done";
page: number;
}): QueryOptions<Flat<{
data: readonly Todo[];
key: readonly ["todos", "list", {
status: "open" | "done";
page: number;
}];
}>>
todosQuery(input: {
status: "open" | "done";
page: number;
}
input: { status: "open" | "done"status: "open" | "done"; page: numberpage: number }) {
return queryOptions<readonly Todo[], unknown, never, readonly ["todos", "list", {
status: "open" | "done";
page: number;
}], undefined>(options: Omit<QueryOptions<{
data: readonly Todo[];
error: unknown;
key: readonly ["todos", "list", {
status: "open" | "done";
page: number;
}];
services: never;
}>, "kind" | "throwOnError"> & ThrowOnErrorField<undefined>): QueryOptions<Flat<{
data: readonly Todo[];
key: readonly ["todos", "list", {
status: "open" | "done";
page: number;
}];
}>>
queryOptions({
queryKey: readonly ["todos", "list", {
status: "open" | "done";
page: number;
}]
queryKey: ["todos", "list", input: {
status: "open" | "done";
page: number;
}
input],
function queryFn(context: QueryFunctionContext): QueryFnResult<{
data: readonly Todo[];
error: unknown;
key: readonly ["todos", "list", {
status: "open" | "done";
page: number;
}];
services: never;
}>
queryFn: () => function listTodos(input?: {
page?: number;
status?: "open" | "done";
}): Promise<readonly Todo[]>
listTodos(input: {
status: "open" | "done";
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
Filters match keys by prefix, and object segments match partially. Ordering a key from general to specific lets one call reach a whole group:
["issues", "list", { projectId: numberprojectId: 3, status: stringstatus: "open" }];
["issues", "detail", 42];
["issues", "comments", 42];
await const qc: QueryClient<never>qc.QueryClient<never>.invalidate<QueryFactory<never>>(...target: Target<QueryFactory<never>>): Promise<void>invalidate({ Filter.queryKey?: QueryKey | undefinedqueryKey: ["issues"] }); // all three
await const qc: QueryClient<never>qc.QueryClient<never>.invalidate<QueryFactory<never>>(...target: Target<QueryFactory<never>>): Promise<void>invalidate({ Filter.queryKey?: QueryKey | undefinedqueryKey: ["issues", "list", { projectId: numberprojectId: 3 }] }); // every list in project 3See Query invalidation for every filter option.