Guides
Query factories
Define each query once as a factory of one serializable input, shared by loaders, server components and components.
A query is a factory: a function of zero or one argument that returns queryOptions(...) or infiniteQueryOptions(...). The loader and the component call the same factory, so data types flow end to end.
import { function infiniteQueryOptions<TPage, TPageParam, TError = unknown, TServices = never, const TKey extends QueryKey = QueryKey, TThrow extends boolean | undefined = undefined>(options: Omit<InfiniteQueryOptions<{
error: TError;
key: TKey;
page: TPage;
pageParam: TPageParam;
services: TServices;
}>, "kind" | "throwOnError"> & ThrowOnErrorField<TThrow>): InfiniteQueryOptions<Flat<{
key: TKey;
page: TPage;
pageParam: TPageParam;
} & ErrorPart<TError> & ServicesPart<TServices> & ThrowPart<...>>>
infiniteQueryOptions, function queryOptions<TData, TError = unknown, TServices = never, const TKey extends QueryKey = QueryKey, TThrow extends boolean | undefined = undefined>(options: Omit<QueryOptions<{
data: TData;
error: TError;
key: TKey;
services: TServices;
}>, "kind" | "throwOnError"> & ThrowOnErrorField<TThrow>): QueryOptions<Flat<{
data: TData;
key: TKey;
} & ErrorPart<TError> & ServicesPart<TServices> & ThrowPart<TThrow>>>
queryOptions } from "@raf-query/react";
export function function postQuery(input: {
slug: string;
}): QueryOptions<Flat<{
data: Post;
key: readonly ["post", string];
}>>
postQuery(input: {
slug: string;
}
input: { slug: stringslug: string }) {
return queryOptions<Post, unknown, never, readonly ["post", string], undefined>(options: Omit<QueryOptions<{
data: Post;
error: unknown;
key: readonly ["post", string];
services: never;
}>, "kind" | "throwOnError"> & ThrowOnErrorField<undefined>): QueryOptions<Flat<{
data: Post;
key: readonly ["post", string];
}>>
queryOptions({
queryKey: readonly ["post", string]queryKey: ["post", input: {
slug: string;
}
input.slug: stringslug],
function queryFn(context: QueryFunctionContext): QueryFnResult<{
data: Post;
error: unknown;
key: readonly ["post", string];
services: never;
}>
queryFn: () => function getPost(slug: string): Promise<Post>getPost(input: {
slug: string;
}
input.slug: stringslug),
staleTime?: number | undefinedstaleTime: 30_000,
});
}
export function function commentsQuery(input: {
slug: string;
}): InfiniteQueryOptions<Flat<{
key: readonly ["comments", string];
page: CommentPage;
pageParam: number;
}>>
commentsQuery(input: {
slug: string;
}
input: { slug: stringslug: string }) {
return infiniteQueryOptions<CommentPage, number, unknown, never, readonly ["comments", string], undefined>(options: Omit<InfiniteQueryOptions<{
error: unknown;
key: readonly ["comments", string];
page: CommentPage;
pageParam: number;
services: never;
}>, "kind" | "throwOnError"> & ThrowOnErrorField<undefined>): InfiniteQueryOptions<Flat<{
key: readonly ["comments", string];
page: CommentPage;
pageParam: number;
}>>
infiniteQueryOptions({
queryKey: readonly ["comments", string]queryKey: ["comments", input: {
slug: string;
}
input.slug: stringslug],
function queryFn(context: PageFunctionContext<number>): QueryFnResult<{
data: CommentPage;
error: unknown;
services: never;
}>
queryFn: (context: PageFunctionContext<number>context) =>
function listComments(input: {
cursor: number;
slug: string;
}): Promise<CommentPage>
listComments({ slug: stringslug: input: {
slug: string;
}
input.slug: stringslug, cursor: numbercursor: context: PageFunctionContext<number>context.PageFunctionContext<number>.pageParam: numberpageParam }),
initialPageParam: numberinitialPageParam: 0,
function getNextPageParam(lastPage: CommentPage, pages: readonly CommentPage[], lastPageParam: number, pageParams: readonly number[]): number | null | undefinedgetNextPageParam: (lastPage: CommentPagelastPage) => lastPage: CommentPagelastPage.CommentPage.nextCursor: number | nullnextCursor,
});
}One input, serializable
Factories take at most one argument; the type rejects two or more. That argument is the query's input, and it must be serializable, because it's what a handle carries from the loader (or the server) to the component.
Use an object even for one field ({ slug }, { id }): it reads well at call sites and leaves room to grow.
const qc: QueryClient<never>qc.QueryClient<never>.prefetch<(input: {
slug: string;
}) => QueryOptions<Flat<{
data: Post;
key: readonly ["post", string];
}>>>(factory: (input: {
slug: string;
}) => QueryOptions<Flat<{
data: Post;
key: readonly ["post", string];
}>>, input: {
slug: string;
}): QueryHandle<(input: {
slug: string;
}) => QueryOptions<Flat<{
data: Post;
key: readonly ["post", string];
}>>>
prefetch(function postQuery(input: {
slug: string;
}): QueryOptions<Flat<{
data: Post;
key: readonly ["post", string];
}>>
postQuery, { slug: stringslug: "hello-world" });
const qc: QueryClient<never>qc.QueryClient<never>.prefetch<() => InfiniteQueryOptions<Flat<{
key: readonly ["issues"];
page: {
items: Array<never>;
nextCursor: null;
};
pageParam: number;
}>>>(factory: () => InfiniteQueryOptions<Flat<{
key: readonly ["issues"];
page: {
items: Array<never>;
nextCursor: null;
};
pageParam: number;
}>>, input?: undefined): QueryHandle<() => InfiniteQueryOptions<Flat<{
key: readonly ["issues"];
page: {
items: Array<never>;
nextCursor: null;
};
pageParam: number;
}>>>
prefetch(function issuesQuery(): InfiniteQueryOptions<Flat<{
key: readonly ["issues"];
page: {
items: Array<never>;
nextCursor: null;
};
pageParam: number;
}>>
issuesQuery); // a factory without input
A factory with two parameters doesn't type-check where it's used:
function function postPageQuery(slug: string, page: number): QueryOptions<Flat<{
data: Post;
key: readonly ["post", string, number];
}>>
postPageQuery(slug: stringslug: string, page: numberpage: number) {
return queryOptions<Post, unknown, never, readonly ["post", string, number], undefined>(options: Omit<QueryOptions<{
data: Post;
error: unknown;
key: readonly ["post", string, number];
services: never;
}>, "kind" | "throwOnError"> & ThrowOnErrorField<undefined>): QueryOptions<Flat<{
data: Post;
key: readonly ["post", string, number];
}>>
queryOptions({
queryKey: readonly ["post", string, number]queryKey: ["post", slug: stringslug, page: numberpage],
function queryFn(context: QueryFunctionContext): QueryFnResult<{
data: Post;
error: unknown;
key: readonly ["post", string, number];
services: never;
}>
queryFn: () => function getPost(slug: string): Promise<Post>getPost(slug: stringslug),
});
}
const qc: QueryClient<never>qc.QueryClient<never>.prefetch<QueryFactory<never>>(factory: QueryFactory<never>, input: unknown): QueryHandle<QueryFactory<never>>prefetch(postPageQuery, "hello-world");The factory decides everything
The factory is the only place a query is declared, including whether it's paged. Every other API — prefetch, ensure, fetch, useQuery, useWaterfall, invalidate, setData — takes either kind and infers the rest from the factory's type: the data, the input, and for paged queries the page and page param types.
Options set in the factory override the client's config.defaults:
| Option | What it does |
|---|---|
queryKey | The cache key; include the input. See Query keys |
queryFn | Loads the data. See Query functions |
staleTime | How long data counts as fresh, in ms |
gcTime | How long an unread entry stays cached, in ms |
retry, retryDelay | See Retries |
refetchOnWindowFocus, refetchOnReconnect | See Window focus refetching |
structuralSharing | See Render optimizations |
enabled | false keeps the query from running. See Disabling a query. Default true |
meta | Anything you want to know about the query elsewhere. See Query meta |
infiniteQueryOptions also takes initialPageParam, getNextPageParam, getPreviousPageParam and maxPages. See Infinite queries.
Disabling a query
As in TanStack Query, set enabled: false to keep a query from running, such as a feature behind a flag:
export function function invoicesQuery(input: {
orgId: string;
}): QueryOptions<Flat<{
data: readonly Invoice[];
key: readonly ["invoices", string];
}>>
invoicesQuery(input: {
orgId: string;
}
input: { orgId: stringorgId: string }) {
return queryOptions<readonly Invoice[], unknown, never, readonly ["invoices", string], undefined>(options: Omit<QueryOptions<{
data: readonly Invoice[];
error: unknown;
key: readonly ["invoices", string];
services: never;
}>, "kind" | "throwOnError"> & ThrowOnErrorField<undefined>): QueryOptions<Flat<{
data: readonly Invoice[];
key: readonly ["invoices", string];
}>>
queryOptions({
queryKey: readonly ["invoices", string]queryKey: ["invoices", input: {
orgId: string;
}
input.orgId: stringorgId],
function queryFn(context: QueryFunctionContext): QueryFnResult<{
data: readonly Invoice[];
error: unknown;
key: readonly ["invoices", string];
services: never;
}>
queryFn: () => function listInvoices(orgId: string): Promise<ReadonlyArray<Invoice>>listInvoices(input: {
orgId: string;
}
input.orgId: stringorgId),
enabled?: boolean | undefinedenabled: const flags: {
readonly billing: boolean;
}
flags.billing: booleanbilling,
});
}Every call runs the factory again, so enabled is decided per call: a later call that returns true, such as the loader running after the flag turned on, starts the query. While it's disabled:
- Nothing runs it: not
prefetch, not its readers, notinvalidateorrefetch().ensureandfetchfail withQuery.DisabledError. - Readers see
isPending. In the browser its promise stays pending, so a reader that callsuse()shows its Suspense fallback until the query is enabled and loads.
enabled: typeof window !== "undefined" disables a query on the server only. See Browser-only queries.
Type inference
TypeScript infers the data type from queryFn. For paged queries it infers the page type from queryFn too, which means queryFn must come before getNextPageParam in the object literal. If your lint setup sorts keys, annotate the page instead:
const const commentsOptions: InfiniteQueryOptions<Flat<{
key: readonly ["comments", "hello-world"];
page: CommentPage;
pageParam: number;
}>>
commentsOptions = infiniteQueryOptions<CommentPage, number, unknown, never, readonly ["comments", "hello-world"], undefined>(options: Omit<InfiniteQueryOptions<{
error: unknown;
key: readonly ["comments", "hello-world"];
page: CommentPage;
pageParam: number;
services: never;
}>, "kind" | "throwOnError"> & ThrowOnErrorField<undefined>): InfiniteQueryOptions<Flat<{
key: readonly ["comments", "hello-world"];
page: CommentPage;
pageParam: number;
}>>
infiniteQueryOptions({
function getNextPageParam(lastPage: CommentPage, pages: readonly CommentPage[], lastPageParam: number, pageParams: readonly number[]): number | null | undefinedgetNextPageParam: (lastPage: CommentPagelastPage: CommentPage) => lastPage: CommentPagelastPage.CommentPage.nextCursor: number | nullnextCursor,
initialPageParam: numberinitialPageParam: 0,
function queryFn(context: PageFunctionContext<number>): QueryFnResult<{
data: CommentPage;
error: unknown;
services: never;
}>
queryFn: (context: PageFunctionContext<number>context) =>
function listComments(input: {
cursor: number;
slug: string;
}): Promise<CommentPage>
listComments({ cursor: numbercursor: context: PageFunctionContext<number>context.PageFunctionContext<number>.pageParam: numberpageParam, slug: stringslug: "hello-world" }),
queryKey: readonly ["comments", "hello-world"]queryKey: ["comments", "hello-world"],
});
commentsOptions;The options' types take a bag of named types rather than a list of type arguments. A promise or plain queryFn leaves error out of that bag (ErrorOf is unknown) and leaves services out when it needs none. An Effect queryFn adds the error it infers, and services when it needs any. The bag also keeps the key's shape as key (readonly ["post", string]), which keeps handles of two factories with the same input and data apart. Annotating options with a bag that has no key, as here, accepts any key:
import type { interface InfiniteQueryOptions<TTypes extends InfiniteQueryTypes>InfiniteQueryOptions, interface QueryOptions<TTypes extends QueryTypes>QueryOptions } from "@raf-query/react";
const const post: QueryOptions<{
data: Post;
}>
post: interface QueryOptions<TTypes extends QueryTypes>QueryOptions<{ data: Postdata: Post }> = function postQuery(input: {
slug: string;
}): QueryOptions<Flat<{
data: Post;
key: readonly ["post", string];
}>>
postQuery({ slug: stringslug: "hello-world" });
const const comments: InfiniteQueryOptions<{
page: CommentPage;
pageParam: number;
}>
comments: interface InfiniteQueryOptions<TTypes extends InfiniteQueryTypes>InfiniteQueryOptions<{ page: CommentPagepage: CommentPage; pageParam: numberpageParam: number }> =
function commentsQuery(input: {
slug: string;
}): InfiniteQueryOptions<Flat<{
key: readonly ["comments", string];
page: CommentPage;
pageParam: number;
}>>
commentsQuery({ slug: stringslug: "hello-world" });To name a factory's types elsewhere:
import type { type DataOf<TFactory extends QueryFactory> = ReturnType<TFactory> extends InfiniteQueryOptions<infer TTypes extends InfiniteQueryTypes> ? InfiniteData<{
page: TTypes["page"];
pageParam: TTypes["pageParam"];
}> : ReturnType<TFactory> extends QueryOptions<infer TTypes extends QueryTypes> ? TTypes["data"] : unknown
DataOf, type InputOf<TFactory extends QueryFactory> = Parameters<TFactory> extends [] ? undefined : Parameters<TFactory>[0]InputOf, interface QueryHandle<TFactory extends QueryFactory = (input: unknown) => FactoryResult>QueryHandle } from "@raf-query/react";
type PostData = type DataOf<TFactory extends QueryFactory> = ReturnType<TFactory> extends InfiniteQueryOptions<infer TTypes extends InfiniteQueryTypes> ? InfiniteData<{
page: TTypes["page"];
pageParam: TTypes["pageParam"];
}> : ReturnType<TFactory> extends QueryOptions<infer TTypes extends QueryTypes> ? TTypes["data"] : unknown
DataOf<typeof function postQuery(input: {
slug: string;
}): QueryOptions<Flat<{
data: Post;
key: readonly ["post", string];
}>>
postQuery>; // what queryFn resolves totype PostInput = type InputOf<TFactory extends QueryFactory> = Parameters<TFactory> extends [] ? undefined : Parameters<TFactory>[0]InputOf<typeof function postQuery(input: {
slug: string;
}): QueryOptions<Flat<{
data: Post;
key: readonly ["post", string];
}>>
postQuery>;type type PostHandle = QueryHandle<(input: {
slug: string;
}) => QueryOptions<Flat<{
data: Post;
key: readonly ["post", string];
}>>>
PostHandle = interface QueryHandle<TFactory extends QueryFactory = (input: unknown) => FactoryResult>QueryHandle<typeof function postQuery(input: {
slug: string;
}): QueryOptions<Flat<{
data: Post;
key: readonly ["post", string];
}>>
postQuery>; // for propsFactory makers
A library can wrap a typed API in a function that returns a factory, generic over the endpoint. Call it inline, wherever a factory goes; the types flow from the endpoint:
function function endpointQuery<TArgs, TData>(endpoint: Endpoint<TArgs, TData>): (args: TArgs) => QueryOptions<Flat<{
data: TData;
key: readonly [string, TArgs];
}>>
endpointQuery<function (type parameter) TArgs in endpointQuery<TArgs, TData>(endpoint: Endpoint<TArgs, TData>): (args: TArgs) => QueryOptions<Flat<{
data: TData;
key: readonly [string, TArgs];
}>>
TArgs, function (type parameter) TData in endpointQuery<TArgs, TData>(endpoint: Endpoint<TArgs, TData>): (args: TArgs) => QueryOptions<Flat<{
data: TData;
key: readonly [string, TArgs];
}>>
TData>(endpoint: Endpoint<TArgs, TData>endpoint: interface Endpoint<TArgs, TData>Endpoint<function (type parameter) TArgs in endpointQuery<TArgs, TData>(endpoint: Endpoint<TArgs, TData>): (args: TArgs) => QueryOptions<Flat<{
data: TData;
key: readonly [string, TArgs];
}>>
TArgs, function (type parameter) TData in endpointQuery<TArgs, TData>(endpoint: Endpoint<TArgs, TData>): (args: TArgs) => QueryOptions<Flat<{
data: TData;
key: readonly [string, TArgs];
}>>
TData>) {
return (args: TArgsargs: function (type parameter) TArgs in endpointQuery<TArgs, TData>(endpoint: Endpoint<TArgs, TData>): (args: TArgs) => QueryOptions<Flat<{
data: TData;
key: readonly [string, TArgs];
}>>
TArgs) =>
queryOptions<TData, unknown, never, readonly [string, TArgs], undefined>(options: Omit<QueryOptions<{
data: TData;
error: unknown;
key: readonly [string, TArgs];
services: never;
}>, "kind" | "throwOnError"> & ThrowOnErrorField<undefined>): QueryOptions<Flat<{
data: TData;
key: readonly [string, TArgs];
}>>
queryOptions({
queryKey: readonly [string, TArgs]queryKey: [endpoint: Endpoint<TArgs, TData>endpoint.Endpoint<TArgs, TData>.name: stringname, args: TArgsargs],
function queryFn(context: QueryFunctionContext): QueryFnResult<{
data: TData;
error: unknown;
key: readonly [string, TArgs];
services: never;
}>
queryFn: (): interface Promise<T>Promise<function (type parameter) TData in endpointQuery<TArgs, TData>(endpoint: Endpoint<TArgs, TData>): (args: TArgs) => QueryOptions<Flat<{
data: TData;
key: readonly [string, TArgs];
}>>
TData> => function call<TArgs, TData>(endpoint: Endpoint<TArgs, TData>, args: TArgs): Promise<TData>call(endpoint: Endpoint<TArgs, TData>endpoint, args: TArgsargs),
});
}
const const post: QueryHandleWithData<(args: {
slug: string;
}) => QueryOptions<Flat<{
data: {
title: string;
};
key: readonly [string, {
slug: string;
}];
}>>>
post = await const qc: QueryClient<never>qc.QueryClient<never>.ensure<(args: {
slug: string;
}) => QueryOptions<Flat<{
data: {
title: string;
};
key: readonly [string, {
slug: string;
}];
}>>>(factory: (args: {
slug: string;
}) => QueryOptions<Flat<{
data: {
title: string;
};
key: readonly [string, {
slug: string;
}];
}>>, input: {
slug: string;
}): Promise<QueryHandleWithData<(args: {
slug: string;
}) => QueryOptions<Flat<{
data: {
title: string;
};
key: readonly [string, {
slug: string;
}];
}>>>> (+1 overload)
ensure(function endpointQuery<{
slug: string;
}, {
title: string;
}>(endpoint: Endpoint<{
slug: string;
}, {
title: string;
}>): (args: {
slug: string;
}) => QueryOptions<Flat<{
data: {
title: string;
};
key: readonly [string, {
slug: string;
}];
}>>
endpointQuery(const api: {
posts: {
get: Endpoint<{
slug: string;
}, {
title: string;
}>;
};
}
api.posts: {
get: Endpoint<{
slug: string;
}, {
title: string;
}>;
}
posts.get: Endpoint<{
slug: string;
}, {
title: string;
}>
get), { slug: stringslug });
const [const postPromise: {
title: string;
}
postPromise] = useQuery<(args: {
slug: string;
}) => QueryOptions<Flat<{
data: {
title: string;
};
key: readonly [string, {
slug: string;
}];
}>>, undefined>(factory: (args: {
slug: string;
}) => QueryOptions<Flat<{
data: {
title: string;
};
key: readonly [string, {
slug: string;
}];
}>>, handle: QueryHandleWithData<(args: {
slug: string;
}) => QueryOptions<Flat<{
data: {
title: string;
};
key: readonly [string, {
slug: string;
}];
}>>>, options?: (Options & {
readonly throwOnError?: never;
} & {
readonly throwOnError?: undefined;
}) | undefined): readonly [data: ...] (+1 overload)
useQuery(function endpointQuery<{
slug: string;
}, {
title: string;
}>(endpoint: Endpoint<{
slug: string;
}, {
title: string;
}>): (args: {
slug: string;
}) => QueryOptions<Flat<{
data: {
title: string;
};
key: readonly [string, {
slug: string;
}];
}>>
endpointQuery(const api: {
posts: {
get: Endpoint<{
slug: string;
}, {
title: string;
}>;
};
}
api.posts: {
get: Endpoint<{
slug: string;
}, {
title: string;
}>;
}
posts.get: Endpoint<{
slug: string;
}, {
title: string;
}>
get), const post: QueryHandleWithData<(args: {
slug: string;
}) => QueryOptions<Flat<{
data: {
title: string;
};
key: readonly [string, {
slug: string;
}];
}>>>
post);Each call returns a new function, but entries are found by queryKey, so every call for the same endpoint and args reaches the same entry. Put everything the key depends on in it.
Server-only factories
A factory whose queryFn imports server-only code (a database client) can't be imported by a client component, so it can't be read with useQuery in the browser. Render its data in a server component, or call the database through a server function or an API route so the browser can refetch it. See React Server Components: server and browser requests and TanStack Start: server and browser requests.