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: PromiseConstructorRepresents the completion of an asynchronous operation
Promise.PromiseConstructor.reject<never>(reason?: any): Promise<never>Creates a new rejected promise for the provided reason.
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 | undefinedAn AbortSignal to set request's signal.
signal: context: QueryFunctionContext<readonly ["posts", string]>context.QueryFunctionContext<readonly ["posts", string]>.signal: AbortSignalAborted when the request is restarted or its entry is removed.
signal });
if (!const response: Responseresponse.Response.ok: booleanThe 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: numberThe 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: AbortSignalAborted 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 itsignal: AbortSignal, aborted when the request is cancelled. Pass it tofetchso cancelled requests stop using the network.meta, the query'smeta, or{}when it has none
Paged queries also get:
pageParam, the page to loaddirection: "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.