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

src/queries.ts
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 | undefined

How 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"),
Argument of type '(slug: string, page: number) => QueryOptions<readonly ["posts", string, number], Post, Post, DefaultTypes>' is not assignable to parameter of type '(input: unknown) => { readonly queryFn: (context: never) => unknown; readonly queryKey: QueryKey; }'. Target signature provides too few arguments. Expected 2 or more, but got 1.
});

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

OptionWhat it does
queryKeyThe cache key; include the input. See Query keys
queryFnLoads the data. See Query functions
selectDerives what readers get from the cached data
staleTimeHow long data counts as fresh, in ms
gcTimeHow long an unread entry stays cached, in ms
structuralSharingSee Render optimizations
streamFolds the chunks of a queryFn that returns an async iterable. See Streaming
metaSettings 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;
const commentsOptions: InfiniteQueryOptions<readonly ["comments", "hello-world"], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>

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 get
type PostData = Post
type 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 PostInput = {
    slug: string;
}
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 props

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