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 group<const TName extends string>(name: TName): Group<readonly [TName]> (+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 infiniteQueryOptions: InfiniteQueryOptionsBuilder<DefaultTypes>Typed options for a paged query factory. Readers get { pages, pageParams }
and, on their state, fetchNextPage and fetchPreviousPage:
infiniteQueryOptions, const queryOptions: QueryOptionsBuilder<DefaultTypes>Typed options for a query factory:
queryOptions } from "@raf-query/react";
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 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: const posts: Group
<readonly ["bySlug", string]>(rest_0: "bySlug", rest_1: string) => readonly ["posts", "bySlug", string]
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),
BaseOptions<TKey extends QueryKey, TTypes extends RootTypes = RootTypes>.staleTime?: number | undefinedHow long data stays fresh, in ms. Default 0.
staleTime: 30_000,
});
}
export function function commentsQuery(input: {
slug: string;
}): InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>
commentsQuery(input: {
slug: string;
}
input: { slug: stringslug: string }) {
return infiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>>(options: InfiniteOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes> & {
readonly queryFn: (context: InfiniteQueryFunctionContext<readonly ["posts", "comments", string], number>) => CommentPage | PromiseLike<CommentPage>;
}): InfiniteQueryOptions<...>
Typed options for a paged query factory. Readers get { pages, pageParams }
and, on their state, fetchNextPage and fetchPreviousPage:
infiniteQueryOptions({
BaseOptions<readonly ["posts", "comments", string], DefaultTypes>.queryKey: readonly ["posts", "comments", string]A plain array, usually built by a group(). Handles keep its literal type.
queryKey: const posts: Group
<readonly ["comments", string]>(rest_0: "comments", rest_1: string) => readonly ["posts", "comments", string]
posts("comments", input: {
slug: string;
}
input.slug: stringslug),
queryFn: (context: InfiniteQueryFunctionContext<readonly ["posts", "comments", string], number>) => CommentPage | PromiseLike<CommentPage>queryFn: (context: InfiniteQueryFunctionContext<readonly ["posts", "comments", string], number>context) =>
function listComments(input: {
cursor: number;
slug: string;
}): Promise<CommentPage>
listComments({ slug: stringslug: input: {
slug: string;
}
input.slug: stringslug, cursor: numbercursor: context: InfiniteQueryFunctionContext<readonly ["posts", "comments", string], number>context.InfiniteQueryFunctionContext<readonly ["posts", "comments", string], number>.pageParam: numberpageParam }),
InfiniteOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>.initialPageParam: numberinitialPageParam: 0,
InfiniteOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>.getNextPageParam: PageParamFn<CommentPage, number>getNextPageParam: (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.
await load<{
readonly post: LoadEntry<(input: {
slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>, "prefetch">;
readonly feed: LoadEntry<() => InfiniteQueryOptions<readonly ["posts", "feed"], PostPage, number, InfiniteData<PostPage, number>, DefaultTypes>, "prefetch">;
}>(client: QueryClient, entries: {
readonly post: LoadEntry<(input: {
slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>, "prefetch">;
readonly feed: LoadEntry<() => InfiniteQueryOptions<readonly ["posts", "feed"], PostPage, number, InfiniteData<PostPage, number>, DefaultTypes>, "prefetch">;
}): Promise<...>
Start a loader's queries and return a serializable handle for each. It
starts every entry before it waits for any, so the order of the entries
can't create a waterfall. Resolves with [handles, data] once every
swr and revalidate entry has data, and rejects with the error as soon
as one fails.
prefetch entries never make it reject. A skipToken in place of an
entry starts nothing and gets no handle.
load(const qc: QueryClient<{
readonly meta: Flat<unknown>;
}>
qc, {
post: LoadEntry<(input: {
slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>, "prefetch">
post: prefetch<(input: {
slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>(factory: (input: {
slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>, ...args: LoadEntryArgs<(input: {
slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>): LoadEntry<(input: {
slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>, "prefetch">
Start the query if the cache has no fresh data, and don't wait for it.
It never makes the load reject: errors show up where a component reads
the handle.
prefetch(function postQuery(input: {
slug: string;
}): QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>
postQuery, { slug: stringslug: "hello-world" }),
feed: LoadEntry<() => InfiniteQueryOptions<readonly ["posts", "feed"], PostPage, number, InfiniteData<PostPage, number>, DefaultTypes>, "prefetch">feed: prefetch<() => InfiniteQueryOptions<readonly ["posts", "feed"], PostPage, number, InfiniteData<PostPage, number>, DefaultTypes>>(factory: () => InfiniteQueryOptions<readonly ["posts", "feed"], PostPage, number, InfiniteData<PostPage, number>, DefaultTypes>, ...args: LoadEntryArgs<() => InfiniteQueryOptions<readonly ["posts", "feed"], PostPage, number, InfiniteData<PostPage, number>, DefaultTypes>>): LoadEntry<...>Start the query if the cache has no fresh data, and don't wait for it.
It never makes the load reject: errors show up where a component reads
the handle.
prefetch(function feedQuery(): InfiniteQueryOptions<readonly ["posts", "feed"], PostPage, number, InfiniteData<PostPage, number>, DefaultTypes>feedQuery), // 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<readonly ["posts", string, number], Post, Post, DefaultTypes>postPageQuery(slug: stringslug: string, page: numberpage: number) {
return queryOptions<readonly ["posts", string, number], Post, Post>(options: BaseOptions<readonly ["posts", string, number], DefaultTypes> & {
readonly queryFn: (context: QueryFunctionContext<readonly ["posts", string, number]>) => Awaitable<Post>;
readonly select?: ((data: Post) => Post) | undefined;
}): QueryOptions<readonly ["posts", string, number], Post, Post, DefaultTypes> (+1 overload)
Typed options for a query factory:
queryOptions({
BaseOptions<readonly ["posts", string, number], DefaultTypes>.queryKey: readonly ["posts", string, number]A plain array, usually built by a group(). Handles keep its literal type.
queryKey: const posts: Group
<readonly [string, number]>(rest_0: string, rest_1: number) => readonly ["posts", string, number]
posts(slug: stringslug, page: numberpage),
queryFn: (context: QueryFunctionContext<readonly ["posts", string, number]>) => Awaitable<Post>queryFn: () => function getPost(slug: string): Promise<Post>getPost(slug: stringslug),
});
}
await load<{
readonly post: LoadEntry<(input: unknown) => {
readonly queryFn: (context: never) => unknown;
readonly queryKey: QueryKey;
}, "prefetch">;
}>(client: QueryClient, entries: {
readonly post: LoadEntry<(input: unknown) => {
readonly queryFn: (context: never) => unknown;
readonly queryKey: QueryKey;
}, "prefetch">;
}): Promise<Loaded<{
readonly post: LoadEntry<(input: unknown) => {
readonly queryFn: (context: never) => unknown;
readonly queryKey: QueryKey;
}, "prefetch">;
}>>
Start a loader's queries and return a serializable handle for each. It
starts every entry before it waits for any, so the order of the entries
can't create a waterfall. Resolves with [handles, data] once every
swr and revalidate entry has data, and rejects with the error as soon
as one fails.
prefetch entries never make it reject. A skipToken in place of an
entry starts nothing and gets no handle.
load(const qc: QueryClient<{
readonly meta: Flat<unknown>;
}>
qc, {
post: LoadEntry<(input: unknown) => {
readonly queryFn: (context: never) => unknown;
readonly queryKey: QueryKey;
}, "prefetch">
post: prefetch<(input: unknown) => {
readonly queryFn: (context: never) => unknown;
readonly queryKey: QueryKey;
}>(factory: (input: unknown) => {
readonly queryFn: (context: never) => unknown;
readonly queryKey: QueryKey;
}, ...args: LoadEntryArgs<(input: unknown) => {
readonly queryFn: (context: never) => unknown;
readonly queryKey: QueryKey;
}>): LoadEntry<(input: unknown) => {
readonly queryFn: (context: never) => unknown;
readonly queryKey: QueryKey;
}, "prefetch">
Start the query if the cache has no fresh data, and don't wait for it.
It never makes the load reject: errors show up where a component reads
the handle.
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, swr, revalidate, waterfallHandle, invalidateOne, 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. Components read the two kinds with their own hooks, useQuery and useInfiniteQuery.
Options set in the factory override the client's (staleTime, gcTime, structuralSharing):
| Option | What it does |
|---|---|
queryKey | The cache key; include the input. See Query keys |
queryFn | Loads the data. See Query functions |
select | Derives what readers get from the cached data |
staleTime | How long data counts as fresh, in ms |
gcTime | How long an unread entry stays cached, in ms |
structuralSharing | See Render optimizations |
stream | Folds the chunks of a queryFn that returns an async iterable. See Streaming |
meta | Settings for the client's links and features, such as retry. See Query meta |
Per-query retries, polling and focus refetching are meta fields, read by the link or feature that does them: meta: { retry: 1 } with retry(), meta: { refetchInterval: 5000 } with polling(). See Retries, Polling and Window focus refetching.
infiniteQueryOptions also takes initialPageParam, getNextPageParam, getPreviousPageParam and maxPages. See Infinite 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<readonly ["comments", "hello-world"], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>commentsOptions = infiniteQueryOptions<readonly ["comments", "hello-world"], CommentPage, number, InfiniteData<CommentPage, number>>(options: InfiniteOptions<readonly ["comments", "hello-world"], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes> & {
readonly queryFn: (context: InfiniteQueryFunctionContext<readonly ["comments", "hello-world"], number>) => CommentPage | PromiseLike<CommentPage>;
}): InfiniteQueryOptions<...>
Typed options for a paged query factory. Readers get { pages, pageParams }
and, on their state, fetchNextPage and fetchPreviousPage:
infiniteQueryOptions({
InfiniteOptions<readonly ["comments", "hello-world"], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>.getNextPageParam: PageParamFn<CommentPage, number>getNextPageParam: (lastPage: CommentPagelastPage: CommentPage) => lastPage: CommentPagelastPage.CommentPage.nextCursor: number | nullnextCursor,
InfiniteOptions<readonly ["comments", "hello-world"], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>.initialPageParam: numberinitialPageParam: 0,
queryFn: (context: InfiniteQueryFunctionContext<readonly ["comments", "hello-world"], number>) => CommentPage | PromiseLike<CommentPage>queryFn: (context: InfiniteQueryFunctionContext<readonly ["comments", "hello-world"], number>context) =>
function listComments(input: {
cursor: number;
slug: string;
}): Promise<CommentPage>
listComments({ cursor: numbercursor: context: InfiniteQueryFunctionContext<readonly ["comments", "hello-world"], number>context.InfiniteQueryFunctionContext<readonly ["comments", "hello-world"], number>.pageParam: numberpageParam, slug: stringslug: "hello-world" }),
BaseOptions<readonly ["comments", "hello-world"], DefaultTypes>.queryKey: readonly ["comments", "hello-world"]A plain array, usually built by a group(). Handles keep its literal type.
queryKey: const comments: Group
<readonly ["hello-world"]>(rest_0: "hello-world") => readonly ["comments", "hello-world"]
comments("hello-world"),
});
commentsOptions;To name a factory's types elsewhere:
import type { type DataOf<TFactory extends QueryFactory> = ReturnType<TFactory> extends {
readonly [dataTag]?: infer TData;
} ? TData : never
What a reader gets: the selected data, or else the queryFn's.
DataOf, type InputOf<TFactory extends QueryFactory> = Parameters<TFactory> extends [] ? undefined : Parameters<TFactory>[0]A factory's input: its first parameter, or undefined if it takes none.
InputOf, interface QueryHandle<TFactory extends QueryFactory = (input: unknown) => { readonly queryFn: (context: never) => unknown; readonly queryKey: QueryKey; }>A small serializable reference to a query: its key and its factory's
input. It carries no data; readers get that from the cache. Handles of
different factories have different types.
QueryHandle } from "@raf-query/react";
type PostData = type DataOf<TFactory extends QueryFactory> = ReturnType<TFactory> extends {
readonly [dataTag]?: infer TData;
} ? TData : never
What a reader gets: the selected data, or else the queryFn's.
DataOf<typeof function postQuery(input: {
slug: string;
}): QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>
postQuery>; // what readers gettype PostInput = type InputOf<TFactory extends QueryFactory> = Parameters<TFactory> extends [] ? undefined : Parameters<TFactory>[0]A factory's input: its first parameter, or undefined if it takes none.
InputOf<typeof function postQuery(input: {
slug: string;
}): QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>
postQuery>;type type PostHandle = QueryHandle<(input: {
slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>
PostHandle = interface QueryHandle<TFactory extends QueryFactory = (input: unknown) => { readonly queryFn: (context: never) => unknown; readonly queryKey: QueryKey; }>A small serializable reference to a query: its key and its factory's
input. It carries no data; readers get that from the cache. Handles of
different factories have different types.
QueryHandle<typeof function postQuery(input: {
slug: string;
}): QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>
postQuery>; // for propsThe key's shape is part of the factory's type too (readonly ["posts", "bySlug", string]), so handles of two factories with the same input and data are still different types.
Factory makers
A library can wrap a typed API in a function that returns a factory, generic over the endpoint. Name the factory it returns, and use that wherever a factory goes; the types flow from the endpoint:
function function endpointQuery<TArgs, TData>(endpoint: Endpoint<TArgs, TData>): (args: TArgs) => QueryOptions<readonly [string, TArgs], TData, TData, DefaultTypes>endpointQuery<function (type parameter) TArgs in endpointQuery<TArgs, TData>(endpoint: Endpoint<TArgs, TData>): (args: TArgs) => QueryOptions<readonly [string, TArgs], TData, TData, DefaultTypes>TArgs, function (type parameter) TData in endpointQuery<TArgs, TData>(endpoint: Endpoint<TArgs, TData>): (args: TArgs) => QueryOptions<readonly [string, TArgs], TData, TData, DefaultTypes>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<readonly [string, TArgs], TData, TData, DefaultTypes>TArgs, function (type parameter) TData in endpointQuery<TArgs, TData>(endpoint: Endpoint<TArgs, TData>): (args: TArgs) => QueryOptions<readonly [string, TArgs], TData, TData, DefaultTypes>TData>) {
const const own: Group<readonly [string]>own = group<string>(name: string): Group<readonly [string]> (+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(endpoint: Endpoint<TArgs, TData>endpoint.Endpoint<TArgs, TData>.name: stringname);
return (args: TArgsargs: function (type parameter) TArgs in endpointQuery<TArgs, TData>(endpoint: Endpoint<TArgs, TData>): (args: TArgs) => QueryOptions<readonly [string, TArgs], TData, TData, DefaultTypes>TArgs) =>
queryOptions<readonly [string, TArgs], TData, TData>(options: BaseOptions<readonly [string, TArgs], DefaultTypes> & {
readonly queryFn: (context: QueryFunctionContext<readonly [string, TArgs]>) => Awaitable<TData>;
readonly select?: ((data: FromQueryFn<TData>) => TData) | undefined;
}): QueryOptions<readonly [string, TArgs], TData, TData, DefaultTypes> (+1 overload)
Typed options for a query factory:
queryOptions({
BaseOptions<readonly [string, TArgs], DefaultTypes>.queryKey: readonly [string, TArgs]A plain array, usually built by a group(). Handles keep its literal type.
queryKey: const own: Group
<readonly [TArgs]>(rest_0: TArgs) => readonly [string, TArgs]
own(args: TArgsargs),
queryFn: (context: QueryFunctionContext<readonly [string, TArgs]>) => Awaitable<TData>queryFn: (): interface Promise<T>Represents the completion of an asynchronous operation
Promise<function (type parameter) TData in endpointQuery<TArgs, TData>(endpoint: Endpoint<TArgs, TData>): (args: TArgs) => QueryOptions<readonly [string, TArgs], TData, TData, DefaultTypes>TData> => function call<TArgs, TData>(endpoint: Endpoint<TArgs, TData>, args: TArgs): Promise<TData>call(endpoint: Endpoint<TArgs, TData>endpoint, args: TArgsargs),
});
}
const const getPostQuery: (args: {
slug: string;
}) => QueryOptions<readonly [string, {
slug: string;
}], {
title: string;
}, {
title: string;
}, DefaultTypes>
getPostQuery = function endpointQuery<{
slug: string;
}, {
title: string;
}>(endpoint: Endpoint<{
slug: string;
}, {
title: string;
}>): (args: {
slug: string;
}) => QueryOptions<readonly [string, {
slug: string;
}], {
title: string;
}, {
title: string;
}, DefaultTypes>
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 [const handles: {
readonly post: QueryHandle<(args: {
slug: string;
}) => QueryOptions<readonly [string, {
slug: string;
}], {
title: string;
}, {
title: string;
}, DefaultTypes>>;
}
handles] = await load<{
readonly post: LoadEntry<(args: {
slug: string;
}) => QueryOptions<readonly [string, {
slug: string;
}], {
title: string;
}, {
title: string;
}, DefaultTypes>, "swr">;
}>(client: QueryClient, entries: {
readonly post: LoadEntry<(args: {
slug: string;
}) => QueryOptions<readonly [string, {
slug: string;
}], {
title: string;
}, {
title: string;
}, DefaultTypes>, "swr">;
}): Promise<Loaded<{
readonly post: LoadEntry<(args: {
slug: string;
}) => QueryOptions<readonly [string, {
slug: string;
}], {
title: string;
}, {
title: string;
}, DefaultTypes>, "swr">;
}>>
Start a loader's queries and return a serializable handle for each. It
starts every entry before it waits for any, so the order of the entries
can't create a waterfall. Resolves with [handles, data] once every
swr and revalidate entry has data, and rejects with the error as soon
as one fails.
prefetch entries never make it reject. A skipToken in place of an
entry starts nothing and gets no handle.
load(const qc: QueryClient<{
readonly meta: Flat<unknown>;
}>
qc, {
post: LoadEntry<(args: {
slug: string;
}) => QueryOptions<readonly [string, {
slug: string;
}], {
title: string;
}, {
title: string;
}, DefaultTypes>, "swr">
post: swr<(args: {
slug: string;
}) => QueryOptions<readonly [string, {
slug: string;
}], {
title: string;
}, {
title: string;
}, DefaultTypes>>(factory: (args: {
slug: string;
}) => QueryOptions<readonly [string, {
slug: string;
}], {
title: string;
}, {
title: string;
}, DefaultTypes>, ...args: LoadEntryArgs<(args: {
slug: string;
}) => QueryOptions<readonly [string, {
slug: string;
}], {
title: string;
}, {
title: string;
}, DefaultTypes>>): LoadEntry<(args: {
slug: string;
}) => QueryOptions<readonly [string, {
slug: string;
}], {
title: string;
}, {
title: string;
}, DefaultTypes>, "swr">
Use cached data even when it's stale, and wait only when the cache has
none. Stale data resolves at once and refetches in the background, as
HTTP's
stale-while-revalidate
does. The load rejects if the fetch it waits for fails.
swr(const getPostQuery: (args: {
slug: string;
}) => QueryOptions<readonly [string, {
slug: string;
}], {
title: string;
}, {
title: string;
}, DefaultTypes>
getPostQuery, { slug: stringslug }),
});
const [const postThenable: QueryValue<(args: {
slug: string;
}) => QueryOptions<readonly [string, {
slug: string;
}], {
title: string;
}, {
title: string;
}, DefaultTypes>>
postThenable] = useQuery<(args: {
slug: string;
}) => QueryOptions<readonly [string, {
slug: string;
}], {
title: string;
}, {
title: string;
}, DefaultTypes>>(factory: (args: {
slug: string;
}) => QueryOptions<readonly [string, {
slug: string;
}], {
title: string;
}, {
title: string;
}, DefaultTypes>, handle: QueryHandle<(args: {
slug: string;
}) => QueryOptions<readonly [string, {
slug: string;
}], {
title: string;
}, {
title: string;
}, DefaultTypes>>, options?: UseQueryOptions): readonly [...]
Read a query by its handle.
useQuery(const getPostQuery: (args: {
slug: string;
}) => QueryOptions<readonly [string, {
slug: string;
}], {
title: string;
}, {
title: string;
}, DefaultTypes>
getPostQuery, const handles: {
readonly post: QueryHandle<(args: {
slug: string;
}) => QueryOptions<readonly [string, {
slug: string;
}], {
title: string;
}, {
title: string;
}, DefaultTypes>>;
}
handles.post: QueryHandle<(args: {
slug: string;
}) => QueryOptions<readonly [string, {
slug: string;
}], {
title: string;
}, {
title: string;
}, DefaultTypes>>
post);
const const post: {
title: string;
}
post = use<{
title: string;
}>(usable: React.Usable<{
title: string;
}>): {
title: string;
}
use(const postThenable: QueryValue<(args: {
slug: string;
}) => QueryOptions<readonly [string, {
slug: string;
}], {
title: string;
}, {
title: string;
}, DefaultTypes>>
postThenable);Each call of endpointQuery 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. Invalidating by factory matches by identity, though, so name the factory once, as here, or invalidate a group in its key.
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.