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 functions

What a queryFn may return, how errors are reported, and what the query function context contains.

A query function can be literally any function that returns a value or a promise. The promise should either resolve the data or throw.

Unlike TanStack Query, undefined is a valid result: a queryFn typed Promise<User | undefined> caches undefined like any other value. So getData and a setData updater's previous are undefined both with no data and with undefined data; a reader's isPending tells them apart.

All of these are valid:

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

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

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

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

queryKey
:
const posts: Group
<readonly []>() => readonly ["posts"]
posts
(),
queryFn: (context: InfiniteQueryFunctionContext<readonly ["posts"], number>) => PostPage | PromiseLike<PostPage>queryFn: (context: InfiniteQueryFunctionContext<readonly ["posts"], number>context) => function fetchPostPage(cursor: number): Promise<PostPage>fetchPostPage(context: InfiniteQueryFunctionContext<readonly ["posts"], number>context.InfiniteQueryFunctionContext<readonly ["posts"], number>.pageParam: numberpageParam), InfiniteOptions<readonly ["posts"], PostPage, number, InfiniteData<PostPage, number>, DefaultTypes>.initialPageParam: numberinitialPageParam: 0, InfiniteOptions<readonly ["posts"], PostPage, number, InfiniteData<PostPage, number>, DefaultTypes>.getNextPageParam: PageParamFn<PostPage, number>getNextPageParam: (lastPage: PostPagelastPage) => lastPage: PostPagelastPage.PostPage.nextCursor: number | nullnextCursor, });
queryOptions<readonly ["posts", string], Post, Post>(options: BaseOptions<readonly ["posts", string], DefaultTypes> & {
    readonly queryFn: (context: QueryFunctionContext<readonly ["posts", string]>) => Awaitable<Post>;
    readonly select?: ((data: Post) => Post) | undefined;
}): QueryOptions<readonly ["posts", string], Post, Post, DefaultTypes> (+1 overload)

Typed options for a query factory:

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

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

queryKey
:
const posts: Group
<readonly [string]>(rest_0: string) => readonly ["posts", string]
posts
(const slug: stringslug), queryFn: (context: QueryFunctionContext<readonly ["posts", string]>) => Awaitable<Post>queryFn: () => function fetchPostBySlug(slug: string): Promise<Post>fetchPostBySlug(const slug: stringslug) });
queryOptions<readonly ["posts", string], Post, Post>(options: BaseOptions<readonly ["posts", string], DefaultTypes> & {
    readonly queryFn: (context: QueryFunctionContext<readonly ["posts", string]>) => Awaitable<Post>;
    readonly select?: ((data: Post) => Post) | undefined;
}): QueryOptions<readonly ["posts", string], Post, Post, DefaultTypes> (+1 overload)

Typed options for a query factory:

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

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

queryKey
:
const posts: Group
<readonly [string]>(rest_0: string) => readonly ["posts", string]
posts
(const slug: stringslug),
queryFn: (context: QueryFunctionContext<readonly ["posts", string]>) => Awaitable<Post>queryFn: async () => { const const data: Postdata = await function fetchPostBySlug(slug: string): Promise<Post>fetchPostBySlug(const slug: stringslug); return const data: Postdata; }, });
queryOptions<readonly ["posts", string], Post, Post>(options: BaseOptions<readonly ["posts", string], DefaultTypes> & {
    readonly queryFn: (context: QueryFunctionContext<readonly ["posts", string]>) => Awaitable<Post>;
    readonly select?: ((data: Post) => Post) | undefined;
}): QueryOptions<readonly ["posts", string], Post, Post, DefaultTypes> (+1 overload)

Typed options for a query factory:

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

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

queryKey
:
const posts: Group
<readonly [string]>(rest_0: string) => readonly ["posts", string]
posts
(const slug: stringslug),
queryFn: (context: QueryFunctionContext<readonly ["posts", string]>) => Awaitable<Post>queryFn: (context: QueryFunctionContext<readonly ["posts", string]>context) => function fetchPostBySlug(slug: string): Promise<Post>fetchPostBySlug(context: QueryFunctionContext<readonly ["posts", string]>context.QueryFunctionContext<readonly ["posts", string]>.queryKey: readonly ["posts", string]queryKey[1]), });

In practice the queryFn lives in a factory and closes over the factory's input, which is easier to type than reading it back from the key.

Handling and throwing errors

A query function that throws or returns a rejected promise fails. The error is kept on the query: the first load's error rejects the promise useQuery returns, so use() throws it to the nearest error boundary, and a later background error shows up in the state's error while the old data stays readable.

queryOptions<readonly ["posts", string], unknown, unknown>(options: BaseOptions<readonly ["posts", string], DefaultTypes> & {
    readonly queryFn: (context: QueryFunctionContext<readonly ["posts", string]>) => unknown;
    readonly select?: ((data: unknown) => unknown) | undefined;
}): QueryOptions<readonly ["posts", string], unknown, unknown, DefaultTypes> (+1 overload)

Typed options for a query factory:

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

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

queryKey
:
const posts: Group
<readonly [string]>(rest_0: string) => readonly ["posts", string]
posts
(const slug: stringslug),
queryFn: (context: QueryFunctionContext<readonly ["posts", string]>) => unknownqueryFn: async () => { if (const somethingGoesWrong: booleansomethingGoesWrong) { throw new
var Error: ErrorConstructor
new (message?: string, options?: ErrorOptions) => Error (+1 overload)
Error
("Oh no!");
} if (const somethingElseGoesWrong: booleansomethingElseGoesWrong) { return var Promise: PromiseConstructor

Represents the completion of an asynchronous operation

Promise
.PromiseConstructor.reject<never>(reason?: any): Promise<never>

Creates a new rejected promise for the provided reason.

@paramreason — The reason the promise was rejected.@returnsA new rejected Promise.
reject
(new
var Error: ErrorConstructor
new (message?: string, options?: ErrorOptions) => Error (+1 overload)
Error
("Oh no!"));
} return const data: unknowndata; }, });

With the retry() link, which recommended() includes, failed requests are retried three times before they count as failed.

With fetch and other clients that don't throw

Many clients, such as axios, throw for unsuccessful HTTP responses. fetch doesn't, so throw yourself:

queryOptions<readonly ["posts", string], any, any>(options: BaseOptions<readonly ["posts", string], DefaultTypes> & {
    readonly queryFn: (context: QueryFunctionContext<readonly ["posts", string]>) => any;
    readonly select?: ((data: any) => any) | undefined;
}): QueryOptions<readonly ["posts", string], any, any, DefaultTypes> (+1 overload)

Typed options for a query factory:

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

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

queryKey
:
const posts: Group
<readonly [string]>(rest_0: string) => readonly ["posts", string]
posts
(const slug: stringslug),
queryFn: (context: QueryFunctionContext<readonly ["posts", string]>) => anyqueryFn: async (context: QueryFunctionContext<readonly ["posts", string]>context) => { const const response: Responseresponse = await function fetch(input: string | URL | Request, init?: RequestInit): Promise<Response> (+1 overload)fetch(`/posts/${const slug: stringslug}`, { RequestInit.signal?: AbortSignal | null | undefined

An AbortSignal to set request's signal.

signal
: context: QueryFunctionContext<readonly ["posts", string]>context.QueryFunctionContext<readonly ["posts", string]>.signal: AbortSignal

Aborted when the request is restarted or its entry is removed.

signal
});
if (!const response: Responseresponse.Response.ok: boolean

The ok read-only property of the Response interface contains a Boolean stating whether the response was successful (status in the range 200-299) or not.

ok
) {
throw new
var Error: ErrorConstructor
new (message?: string, options?: ErrorOptions) => Error (+1 overload)
Error
(`Request failed: ${const response: Responseresponse.Response.status: number

The status read-only property of the Response interface contains the HTTP status codes of the response.

status
}`);
} return const response: Responseresponse.Body.json(): Promise<any>json(); }, });

A not-found can also be a plain value: return null and let the page decide, or throw your router's notFound() from the queryFn so the route renders its not-found component.

Streaming

A queryFn that returns an async iterable is a stream. Pass stream to fold its chunks into the data: initial is the data before the first chunk, and reduce adds a chunk to it.

export function 
function logQuery(input: {
    buildId: string;
}): QueryOptions<readonly ["builds", "log", {
    buildId: string;
}], readonly string[], readonly string[], DefaultTypes>
logQuery
(
input: {
    buildId: string;
}
input
: { buildId: stringbuildId: string }) {
return
queryOptions<readonly ["builds", "log", {
    buildId: string;
}], string, readonly string[]>(options: BaseOptions<readonly ["builds", "log", {
    buildId: string;
}], DefaultTypes> & {
    readonly queryFn: (context: QueryFunctionContext<readonly ["builds", "log", {
        buildId: string;
    }]>) => AsyncIterable<string>;
    readonly stream: StreamOptions<readonly string[], string>;
}): QueryOptions<readonly ["builds", "log", {
    buildId: string;
}], readonly string[], readonly string[], DefaultTypes> (+1 overload)

Typed options for a query factory:

queryOptions
({
BaseOptions<readonly ["builds", "log", { buildId: string; }], DefaultTypes>.queryKey: readonly ["builds", "log", {
    buildId: string;
}]

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

queryKey
:
const builds: Group
<readonly ["log", {
    buildId: string;
}]>(rest_0: "log", rest_1: {
    buildId: string;
}) => readonly ["builds", "log", {
    buildId: string;
}]
builds
("log",
input: {
    buildId: string;
}
input
),
queryFn: (context: QueryFunctionContext<readonly ["builds", "log", {
    buildId: string;
}]>) => AsyncIterable<string>
queryFn
: (
context: QueryFunctionContext<readonly ["builds", "log", {
    buildId: string;
}]>
context
) => function streamLines(signal: AbortSignal): AsyncIterable<string>streamLines(
context: QueryFunctionContext<readonly ["builds", "log", {
    buildId: string;
}]>
context
.QueryFunctionContext<readonly ["builds", "log", { buildId: string; }]>.signal: AbortSignal

Aborted when the request is restarted or its entry is removed.

signal
),
stream: StreamOptions<readonly string[], string>stream: { StreamOptions<readonly string[], string>.initial: readonly string[]

The data before the first chunk.

initial
: [] as interface ReadonlyArray<T>ReadonlyArray<string>,
StreamOptions<readonly string[], string>.reduce: (data: readonly string[], chunk: string) => readonly string[]

Fold a chunk into the data.

reduce
: (lines: readonly string[]lines, line: stringline) => [...lines: readonly string[]lines, line: stringline],
}, }); }

Readers get the data after the first chunk, and every later chunk updates it in place. The state's isFetching stays true until the stream ends.

The query function context

Each queryFn receives a context object:

  • queryKey, the query's key, typed as the factory wrote it
  • signal: AbortSignal, aborted when the request is cancelled. Pass it to fetch so cancelled requests stop using the network.
  • meta, the query's meta, or {} when it has none

Paged queries also get:

  • pageParam, the page to load
  • direction: "forward" | "backward", which end of the list the page is added to

Redirects

In TanStack Start, a queryFn can throw redirect({ to: "/login" }). The router integration turns it into a navigation, from a loader or from a background refetch. Pass handleRedirects: false to setupRouterIntegration to handle them yourself.