Skip to content

raf-query is early. The API can still change between minor releases, so use it at your own risk.

raf-query

Getting started

Quick start

Set up a QueryClient, define a query factory, start it in a TanStack Start loader and read it with useQuery and use().

This walks through the three core concepts of raf-query on TanStack Start:

  • Query factories, which describe a query once
  • Starting queries in a loader, which hands components a handle
  • Reading queries with useQuery and use()

For the Next.js App Router, the same steps apply with a server component in place of the loader: see React Server Components. For React Router Framework Mode, see React Router.

1. Create the client

Create one QueryClient per router. On the server, getRouter runs once per request, so each request gets its own cache; in the browser it runs once per tab.

src/router.tsx
import { class QueryClient<TServices = never>QueryClient, 
function QueryClientProvider(props: {
    readonly children?: React.ReactNode;
    readonly client: QueryClient<unknown>;
}): React.JSX.Element
QueryClientProvider
} from "@raf-query/react";
import { function QueryDevtools(props: QueryDevtoolsProps): React.JSX.Element | nullQueryDevtools } from "@raf-query/react/devtools"; import { function setupRouterIntegration<TRouter extends AnyRouter>(options: RouterIntegrationOptions<TRouter>): voidsetupRouterIntegration } from "@raf-query/react/tanstack-router"; import { const createRouter: CreateRouterFncreateRouter } from "@tanstack/react-router"; import {
const routeTree: Route<Register, any, "/", "/", string, "__root__", undefined, {}, {
    queryClient: QueryClient;
}, AnyContext, AnyContext, {}, undefined, RootRouteChildren, FileRouteTypes, unknown, unknown, undefined>
routeTree
} from "./routeTree.gen.ts";
export function
function getRouter(): RouterCore<Route<Register, any, "/", "/", string, "__root__", undefined, {}, {
    queryClient: QueryClient;
}, AnyContext, AnyContext, {}, undefined, RootRouteChildren, FileRouteTypes, unknown, unknown, undefined>, "never", false, RouterHistory, Record<string, any>>
getRouter
() {
const const queryClient: QueryClient<never>queryClient = new new QueryClient<never>(config?: QueryClientConfig<never>): QueryClient<never>QueryClient(); const
const router: RouterCore<Route<Register, any, "/", "/", string, "__root__", undefined, {}, {
    queryClient: QueryClient;
}, AnyContext, AnyContext, {}, undefined, RootRouteChildren, FileRouteTypes, unknown, unknown, undefined>, "never", false, RouterHistory, Record<string, any>>
router
=
createRouter<Route<Register, any, "/", "/", string, "__root__", undefined, {}, {
    queryClient: QueryClient;
}, AnyContext, AnyContext, {}, undefined, RootRouteChildren, FileRouteTypes, unknown, unknown, undefined>, "never", false, RouterHistory, Record<string, any>>(options: RouterConstructorOptions<Route<Register, any, "/", "/", string, "__root__", undefined, {}, {
    queryClient: QueryClient;
}, AnyContext, AnyContext, {}, undefined, RootRouteChildren, FileRouteTypes, unknown, unknown, undefined>, "never", false, RouterHistory, Record<...>>): RouterCore<...>
createRouter
({
context: {
    queryClient: QueryClient;
}
context
: { queryClient: QueryClient<never>queryClient },
defaultPreload?: false | "intent" | "viewport" | "render" | undefineddefaultPreload: "intent", // The query cache decides what's fresh; loaders always run and dedupe there. defaultPreloadStaleTime?: number | undefineddefaultPreloadStaleTime: 0,
routeTree?: Route<Register, any, "/", "/", string, "__root__", undefined, {}, {
    queryClient: QueryClient;
}, AnyContext, AnyContext, {}, undefined, RootRouteChildren, FileRouteTypes, unknown, unknown, undefined> | undefined
routeTree
,
scrollRestoration?: boolean | ((opts: {
    location: ParsedLocation;
}) => boolean) | undefined
scrollRestoration
: true,
Wrap?: ((props: {
    children: any;
}) => React.JSX.Element) | undefined
Wrap
: (
props: {
    children: any;
}
props
) => (
<
function QueryClientProvider(props: {
    readonly children?: React.ReactNode;
    readonly client: QueryClient<unknown>;
}): React.JSX.Element
QueryClientProvider
client: QueryClient<unknown>client={const queryClient: QueryClient<never>queryClient}>
{
props: {
    children: any;
}
props
.children: anychildren}
{import.meta.ImportMeta.env: ImportMetaEnvenv.ImportMetaEnv.DEV: booleanDEV ? <function QueryDevtools(props: QueryDevtoolsProps): React.JSX.Element | nullQueryDevtools /> : null} </
function QueryClientProvider(props: {
    readonly children?: React.ReactNode;
    readonly client: QueryClient<unknown>;
}): React.JSX.Element
QueryClientProvider
>
), });
setupRouterIntegration<RouterCore<Route<Register, any, "/", "/", string, "__root__", undefined, {}, {
    queryClient: QueryClient;
}, AnyContext, AnyContext, {}, undefined, RootRouteChildren, FileRouteTypes, unknown, unknown, undefined>, "never", false, RouterHistory, Record<string, any>>>(options: RouterIntegrationOptions<RouterCore<Route<Register, any, "/", "/", string, "__root__", undefined, {}, {
    queryClient: QueryClient;
}, AnyContext, AnyContext, ... 6 more ..., undefined>, "never", false, RouterHistory, Record<...>>>): void
setupRouterIntegration
({ RouterIntegrationOptions<TRouter extends AnyRouter>.queryClient: QueryClient<never>queryClient,
RouterIntegrationOptions<RouterCore<Route<Register, any, "/", "/", string, "__root__", undefined, {}, { queryClient: QueryClient; }, AnyContext, AnyContext, {}, ... 5 more ..., undefined>, "never", false, RouterHistory, Record<...>>>.router: RouterCore<Route<Register, any, "/", "/", string, "__root__", undefined, {}, {
    queryClient: QueryClient;
}, AnyContext, AnyContext, {}, undefined, RootRouteChildren, FileRouteTypes, unknown, unknown, undefined>, "never", false, RouterHistory, Record<string, any>>
router
});
return
const router: RouterCore<Route<Register, any, "/", "/", string, "__root__", undefined, {}, {
    queryClient: QueryClient;
}, AnyContext, AnyContext, {}, undefined, RootRouteChildren, FileRouteTypes, unknown, unknown, undefined>, "never", false, RouterHistory, Record<string, any>>
router
;
}

setupRouterIntegration streams the server's cache to the browser while the page renders, so data loaded on the server is never fetched twice. In development, DevTools show what's in the cache. Give the root route the client's type:

src/routes/__root.tsx
import type { class QueryClient<TServices = never>QueryClient } from "@raf-query/react";
import { function createRootRouteWithContext<TRouterContext extends {}>(): <TRegister = Register, TRouteContextFn = AnyContext, TBeforeLoadFn = AnyContext, TSearchValidator = undefined, TLoaderDeps extends Record<string, any> = {}, TLoaderFn = undefined, TSSR = unknown, TServerMiddlewares = unknown>(options?: RootRouteOptions<TRegister, TSearchValidator, TRouterContext, TRouteContextFn, TBeforeLoadFn, TLoaderDeps, TLoaderFn, TSSR, TServerMiddlewares>) => RootRoute<TRegister, TSearchValidator, TRouterContext, TRouteContextFn, TBeforeLoadFn, TLoaderDeps, TLoaderFn, unknown, unknown, TSSR, TServerMiddlewares, undefined>createRootRouteWithContext } from "@tanstack/react-router";

export const 
const Route: RootRoute<Register, undefined, {
    queryClient: QueryClient;
}, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>
Route
=
createRootRouteWithContext<{
    queryClient: QueryClient;
}>(): <TRegister, TRouteContextFn, TBeforeLoadFn, TSearchValidator, TLoaderDeps, TLoaderFn, TSSR, TServerMiddlewares>(options?: RootRouteOptions<TRegister, TSearchValidator, {
    queryClient: QueryClient;
}, TRouteContextFn, TBeforeLoadFn, TLoaderDeps, TLoaderFn, TSSR, TServerMiddlewares, undefined> | undefined) => RootRoute<...>
createRootRouteWithContext
<{ queryClient: QueryClient<never>queryClient: class QueryClient<TServices = never>QueryClient }>()(
{}, );

2. Define a query

A query is a factory: a function of at most one argument that returns queryOptions(...). The argument is the query's input. Loaders and components call the same factory, so the data's type flows end to end.

src/queries.ts
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";
import { function getTodo(id: number): Promise<Todo>getTodo,
function listTodos(input?: {
    page?: number;
    status?: "open" | "done";
}): Promise<readonly Todo[]>
listTodos
} from "./api.ts";
export function
function todosQuery(): InfiniteQueryOptions<Flat<{
    key: readonly ["todos"];
    page: {
        items: readonly Todo[];
        nextCursor: null;
    };
    pageParam: number;
}>>
todosQuery
() {
return
infiniteQueryOptions<{
    items: readonly Todo[];
    nextCursor: null;
}, number, unknown, never, readonly ["todos"], undefined>(options: Omit<InfiniteQueryOptions<{
    error: unknown;
    key: readonly ["todos"];
    page: {
        items: readonly Todo[];
        nextCursor: null;
    };
    pageParam: number;
    services: never;
}>, "kind" | "throwOnError"> & ThrowOnErrorField<undefined>): InfiniteQueryOptions<Flat<{
    key: readonly ["todos"];
    page: {
        items: readonly Todo[];
        nextCursor: null;
    };
    pageParam: number;
}>>
infiniteQueryOptions
({
queryKey: readonly ["todos"]queryKey: ["todos"],
function queryFn(context: PageFunctionContext<number>): QueryFnResult<{
    data: {
        items: readonly Todo[];
        nextCursor: null;
    };
    error: unknown;
    services: never;
}>
queryFn
: async () => ({
items: readonly Todo[]items: await
function listTodos(input?: {
    page?: number;
    status?: "open" | "done";
}): Promise<readonly Todo[]>
listTodos
(),
nextCursor: nullnextCursor: null, }), initialPageParam: numberinitialPageParam: 0,
function getNextPageParam(lastPage: {
    items: readonly Todo[];
    nextCursor: null;
}, pages: readonly {
    items: readonly Todo[];
    nextCursor: null;
}[], lastPageParam: number, pageParams: readonly number[]): number | null | undefined
getNextPageParam
: (
lastPage: {
    items: readonly Todo[];
    nextCursor: null;
}
lastPage
) =>
lastPage: {
    items: readonly Todo[];
    nextCursor: null;
}
lastPage
.nextCursor: nullnextCursor,
}); } export function
function todoQuery(input: {
    id: number;
}): QueryOptions<Flat<{
    data: Todo;
    key: readonly ["todos", number];
}>>
todoQuery
(
input: {
    id: number;
}
input
: { id: numberid: number }) {
return
queryOptions<Todo, unknown, never, readonly ["todos", number], undefined>(options: Omit<QueryOptions<{
    data: Todo;
    error: unknown;
    key: readonly ["todos", number];
    services: never;
}>, "kind" | "throwOnError"> & ThrowOnErrorField<undefined>): QueryOptions<Flat<{
    data: Todo;
    key: readonly ["todos", number];
}>>
queryOptions
({
queryKey: readonly ["todos", number]queryKey: ["todos",
input: {
    id: number;
}
input
.id: numberid],
function queryFn(context: QueryFunctionContext): QueryFnResult<{
    data: Todo;
    error: unknown;
    key: readonly ["todos", number];
    services: never;
}>
queryFn
: () => function getTodo(id: number): Promise<Todo>getTodo(
input: {
    id: number;
}
input
.id: numberid),
staleTime?: number | undefinedstaleTime: 30_000, }); }

3. Start it in the loader

The loader starts the query and returns its handle, a small { key, input } object:

src/routes/todos.tsx
import { type interface QueryHandle<TFactory extends QueryFactory = (input: unknown) => FactoryResult>QueryHandle, 
function useQuery<TFactory extends QueryFactory, const TThrow extends boolean | undefined = undefined>(factory: TFactory, handle: QueryHandleWithData<TFactory>, options?: UseQueryOptions & UseQueryThrowOptions<TFactory> & {
    readonly throwOnError?: TThrow;
}): UseQueryResultWithData<TFactory, TThrow> (+1 overload)
useQuery
} from "@raf-query/react";
import { function createFileRoute<TFilePath extends keyof FileRoutesByPath, TParentRoute extends AnyRoute = FileRoutesByPath[TFilePath]["parentRoute"], TId extends RouteConstraints["TId"] = FileRoutesByPath[TFilePath]["id"], TPath extends RouteConstraints["TPath"] = FileRoutesByPath[TFilePath]["path"], TFullPath extends RouteConstraints["TFullPath"] = FileRoutesByPath[TFilePath]["fullPath"]>(path?: TFilePath): FileRoute<TFilePath, TParentRoute, TId, TPath, TFullPath>["createRoute"]createFileRoute } from "@tanstack/react-router"; import { const Suspense: React.ExoticComponent<React.SuspenseProps>Suspense, function use<T>(usable: React.Usable<T>): Tuse } from "react"; import {
function todosQuery(): InfiniteQueryOptions<Flat<{
    key: readonly ["todos"];
    page: {
        items: readonly Todo[];
        nextCursor: null;
    };
    pageParam: number;
}>>
todosQuery
} from "~/queries.ts";
export const
const Route: Route<Register, RootRoute<Register, undefined, {
    queryClient: QueryClient;
}, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/todos", "/todos", "/todos", "/todos", undefined, ResolveParams<"/todos">, AnyContext, AnyContext, AnyContext, {}, (args: LoaderFnContext<Register, RootRoute<Register, undefined, {
    queryClient: QueryClient;
}, AnyContext, ... 7 more ..., undefined>, ... 7 more ..., undefined>) => {
    ...;
}, ... 4 more ..., undefined>
Route
=
createFileRoute<"/todos", RootRoute<Register, undefined, {
    queryClient: QueryClient;
}, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/todos", "/todos", "/todos">(path?: "/todos" | undefined): <TRegister, TSearchValidator, TParams, TRouteContextFn, TBeforeLoadFn, TLoaderDeps, TLoaderFn, TChildren, TSSR, TMiddlewares, THandlers>(options?: (ParamsOptions<...> & ... 1 more ... & UpdatableRouteOptions<...>) | undefined) => Route<...>
createFileRoute
("/todos")({
FilebaseRouteOptionsInterface<Register, RootRoute<Register, undefined, { queryClient: QueryClient; }, AnyContext, AnyContext, {}, ... 5 more ..., undefined>, ... 12 more ..., undefined>.loader?: Constrain<(args: LoaderFnContext<Register, RootRoute<Register, undefined, {
    queryClient: QueryClient;
}, AnyContext, ... 7 more ..., undefined>, ... 7 more ..., undefined>) => {
    ...;
}, RouteLoaderFn<...> | RouteLoaderObject<...>> | undefined
loader
: (
args: LoaderFnContext<Register, RootRoute<Register, undefined, {
    queryClient: QueryClient;
}, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/todos", ResolveParams<"/todos">, {}, AnyContext, AnyContext, AnyContext, unknown, undefined>
args
) => ({
todos: QueryHandle<() => InfiniteQueryOptions<Flat<{
    key: readonly ["todos"];
    page: {
        items: readonly Todo[];
        nextCursor: null;
    };
    pageParam: number;
}>>>
todos
:
args: LoaderFnContext<Register, RootRoute<Register, undefined, {
    queryClient: QueryClient;
}, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/todos", ResolveParams<"/todos">, {}, AnyContext, AnyContext, AnyContext, unknown, undefined>
args
.
LoaderFnContext<Register, RootRoute<Register, undefined, { queryClient: QueryClient; }, AnyContext, AnyContext, ... 6 more ..., undefined>, ... 7 more ..., undefined>.context: {
    queryClient: QueryClient;
}
context
.queryClient: QueryClient<never>queryClient.
QueryClient<never>.prefetch<() => InfiniteQueryOptions<Flat<{
    key: readonly ["todos"];
    page: {
        items: readonly Todo[];
        nextCursor: null;
    };
    pageParam: number;
}>>>(factory: () => InfiniteQueryOptions<Flat<{
    key: readonly ["todos"];
    page: {
        items: readonly Todo[];
        nextCursor: null;
    };
    pageParam: number;
}>>, input?: undefined): QueryHandle<() => InfiniteQueryOptions<Flat<{
    key: readonly ["todos"];
    page: {
        items: readonly Todo[];
        nextCursor: null;
    };
    pageParam: number;
}>>>
prefetch
(
function todosQuery(): InfiniteQueryOptions<Flat<{
    key: readonly ["todos"];
    page: {
        items: readonly Todo[];
        nextCursor: null;
    };
    pageParam: number;
}>>
todosQuery
),
}), UpdatableRouteOptionsExtensions.component?: RouteComponent | undefinedcomponent: function TodosPage(): React.JSX.ElementTodosPage, });

prefetch starts the request and returns immediately, so the page renders while the data is on its way. Use ensure instead when the page can't lay out without the data: the navigation then waits for it. Starting queries covers the trade-off.

4. Read it in a component

src/routes/todos.tsx
function function TodosPage(): React.JSX.ElementTodosPage() {
	const 
const loaderData: {
    todos: QueryHandle<() => InfiniteQueryOptions<Flat<{
        key: readonly ["todos"];
        page: {
            items: readonly Todo[];
            nextCursor: null;
        };
        pageParam: number;
    }>>>;
}
loaderData
=
const Route: Route<Register, RootRoute<Register, undefined, {
    queryClient: QueryClient;
}, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/todos", "/todos", "/todos", "/todos", undefined, ResolveParams<"/todos">, AnyContext, AnyContext, AnyContext, {}, (args: LoaderFnContext<Register, RootRoute<Register, undefined, {
    queryClient: QueryClient;
}, AnyContext, ... 7 more ..., undefined>, ... 7 more ..., undefined>) => {
    ...;
}, ... 4 more ..., undefined>
Route
.
RouteExtensions<"/todos", "/todos">.useLoaderData: <RouterCore<Route<Register, any, "/", "/", string, "__root__", undefined, {}, {
    queryClient: QueryClient;
}, AnyContext, AnyContext, {}, undefined, RootRouteChildren, FileRouteTypes, unknown, unknown, undefined>, "never", false, RouterHistory, Record<string, any>>, unknown, boolean>(opts?: (UseLoaderDataBaseOptions<RouterCore<Route<Register, any, "/", "/", string, "__root__", undefined, {}, {
    queryClient: QueryClient;
}, ... 8 more ..., undefined>, "never", false, RouterHistory, Record<...>>, "/todos", true, unknown, boolean> & OptionalStructuralSharing<...>) | undefined) => {
    ...;
}
useLoaderData
();
return ( <const Suspense: React.ExoticComponent<React.SuspenseProps>Suspense React.SuspenseProps.fallback?: React.ReactNodefallback={<React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>Loading…</React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>}> <
function TodoList(props: {
    handle: QueryHandle<typeof todosQuery>;
}): React.JSX.Element
TodoList
handle: QueryHandle<() => InfiniteQueryOptions<Flat<{
    key: readonly ["todos"];
    page: {
        items: readonly Todo[];
        nextCursor: null;
    };
    pageParam: number;
}>>>
handle
={
const loaderData: {
    todos: QueryHandle<() => InfiniteQueryOptions<Flat<{
        key: readonly ["todos"];
        page: {
            items: readonly Todo[];
            nextCursor: null;
        };
        pageParam: number;
    }>>>;
}
loaderData
.
todos: QueryHandle<() => InfiniteQueryOptions<Flat<{
    key: readonly ["todos"];
    page: {
        items: readonly Todo[];
        nextCursor: null;
    };
    pageParam: number;
}>>>
todos
} />
</const Suspense: React.ExoticComponent<React.SuspenseProps>Suspense> ); } function
function TodoList(props: {
    handle: QueryHandle<typeof todosQuery>;
}): React.JSX.Element
TodoList
(
props: {
    handle: QueryHandle<typeof todosQuery>;
}
props
: {
handle: QueryHandle<() => InfiniteQueryOptions<Flat<{
    key: readonly ["todos"];
    page: {
        items: readonly Todo[];
        nextCursor: null;
    };
    pageParam: number;
}>>>
handle
: interface QueryHandle<TFactory extends QueryFactory = (input: unknown) => FactoryResult>QueryHandle<typeof
function todosQuery(): InfiniteQueryOptions<Flat<{
    key: readonly ["todos"];
    page: {
        items: readonly Todo[];
        nextCursor: null;
    };
    pageParam: number;
}>>
todosQuery
> }) {
const [
const pagesPromise: Thenable<InfiniteData<{
    page: {
        items: readonly Todo[];
        nextCursor: null;
    };
    pageParam: number;
}>>
pagesPromise
,
const todosState: QueryControls<() => InfiniteQueryOptions<Flat<{
    key: readonly ["todos"];
    page: {
        items: readonly Todo[];
        nextCursor: null;
    };
    pageParam: number;
}>>> & PagingControls
todosState
] =
useQuery<() => InfiniteQueryOptions<Flat<{
    key: readonly ["todos"];
    page: {
        items: readonly Todo[];
        nextCursor: null;
    };
    pageParam: number;
}>>, undefined>(factory: () => InfiniteQueryOptions<Flat<{
    key: readonly ["todos"];
    page: {
        items: readonly Todo[];
        nextCursor: null;
    };
    pageParam: number;
}>>, handle: QueryHandle<() => InfiniteQueryOptions<Flat<{
    key: readonly ["todos"];
    page: {
        items: readonly Todo[];
        nextCursor: null;
    };
    pageParam: number;
}>>>, options?: (Options & {
    readonly throwOnError?: never;
} & {
    readonly throwOnError?: undefined;
}) | undefined): readonly [promise: ...] (+1 overload)
useQuery
(
function todosQuery(): InfiniteQueryOptions<Flat<{
    key: readonly ["todos"];
    page: {
        items: readonly Todo[];
        nextCursor: null;
    };
    pageParam: number;
}>>
todosQuery
,
props: {
    handle: QueryHandle<typeof todosQuery>;
}
props
.
handle: QueryHandle<() => InfiniteQueryOptions<Flat<{
    key: readonly ["todos"];
    page: {
        items: readonly Todo[];
        nextCursor: null;
    };
    pageParam: number;
}>>>
handle
);
const
const pages: InfiniteData<{
    page: {
        items: readonly Todo[];
        nextCursor: null;
    };
    pageParam: number;
}>
pages
=
use<InfiniteData<{
    page: {
        items: readonly Todo[];
        nextCursor: null;
    };
    pageParam: number;
}>>(usable: React.Usable<InfiniteData<{
    page: {
        items: readonly Todo[];
        nextCursor: null;
    };
    pageParam: number;
}>>): InfiniteData<{
    page: {
        items: readonly Todo[];
        nextCursor: null;
    };
    pageParam: number;
}>
use
(
const pagesPromise: Thenable<InfiniteData<{
    page: {
        items: readonly Todo[];
        nextCursor: null;
    };
    pageParam: number;
}>>
pagesPromise
);
return ( <React.JSX.IntrinsicElements.ul: React.DetailedHTMLProps<React.HTMLAttributes<HTMLUListElement>, HTMLUListElement>ul React.AriaAttributes["aria-busy"]?: Booleanish | undefinedaria-busy={
const todosState: QueryControls<() => InfiniteQueryOptions<Flat<{
    key: readonly ["todos"];
    page: {
        items: readonly Todo[];
        nextCursor: null;
    };
    pageParam: number;
}>>> & PagingControls
todosState
.QueryControls<() => InfiniteQueryOptions<Flat<{ key: readonly ["todos"]; page: { items: readonly Todo[]; nextCursor: null; }; pageParam: number; }>>>.isFetching: booleanisFetching}>
{
const pages: InfiniteData<{
    page: {
        items: readonly Todo[];
        nextCursor: null;
    };
    pageParam: number;
}>
pages
.
InfiniteData<{ page: { items: readonly Todo[]; nextCursor: null; }; pageParam: number; }>.pages: readonly {
    items: readonly Todo[];
    nextCursor: null;
}[]
pages
.
ReadonlyArray<{ items: readonly Todo[]; nextCursor: null; }>.flatMap<Todo, undefined>(callback: (this: undefined, value: {
    items: readonly Todo[];
    nextCursor: null;
}, index: number, array: {
    items: readonly Todo[];
    nextCursor: null;
}[]) => Todo | readonly Todo[], thisArg?: undefined): Todo[]
flatMap
((
page: {
    items: readonly Todo[];
    nextCursor: null;
}
page
) =>
page: {
    items: readonly Todo[];
    nextCursor: null;
}
page
.items: readonly Todo[]items)
.Array<Todo>.map<React.JSX.Element>(callbackfn: (value: Todo, index: number, array: Todo[]) => React.JSX.Element, thisArg?: any): React.JSX.Element[]map((todo: Todotodo) => ( <React.JSX.IntrinsicElements.li: React.DetailedHTMLProps<React.LiHTMLAttributes<HTMLLIElement>, HTMLLIElement>li React.Attributes.key?: React.Key | null | undefinedkey={todo: Todotodo.Todo.id: numberid}>{todo: Todotodo.Todo.title: stringtitle}</React.JSX.IntrinsicElements.li: React.DetailedHTMLProps<React.LiHTMLAttributes<HTMLLIElement>, HTMLLIElement>li> ))} </React.JSX.IntrinsicElements.ul: React.DetailedHTMLProps<React.HTMLAttributes<HTMLUListElement>, HTMLUListElement>ul> ); }

useQuery returns a stable promise and the query's controls. use() suspends until the data is there, then returns it; once the cache has data, the promise is already resolved and use() returns synchronously.

5. Write data with an Action

There is no useMutation. Write in a form action, then invalidate what changed:

src/components/add-todo.tsx
export function function AddTodo(): React.JSX.ElementAddTodo() {
	const const qc: QueryClient<unknown>qc = function useQueryClient(): QueryClient<unknown>useQueryClient();
	return (
		<React.JSX.IntrinsicElements.form: React.DetailedHTMLProps<React.FormHTMLAttributes<HTMLFormElement>, HTMLFormElement>form
			React.FormHTMLAttributes<HTMLFormElement>.action?: string | ((formData: FormData) => void | Promise<void>) | undefinedaction={async (formData: FormDataformData) => {
				await 
function createTodo(input: {
    title: string;
}): Promise<Todo>
createTodo
({ title: stringtitle:
var String: StringConstructor
(value?: any) => string
String
(formData: FormDataformData.FormData.get(name: string): FormDataEntryValue | nullget("title")) });
await const qc: QueryClient<unknown>qc.
QueryClient<unknown>.invalidate<() => InfiniteQueryOptions<Flat<{
    key: readonly ["todos"];
    page: {
        items: readonly Todo[];
        nextCursor: null;
    };
    pageParam: number;
}>>>(...target: Target<() => InfiniteQueryOptions<Flat<{
    key: readonly ["todos"];
    page: {
        items: readonly Todo[];
        nextCursor: null;
    };
    pageParam: number;
}>>>): Promise<void>
invalidate
(
function todosQuery(): InfiniteQueryOptions<Flat<{
    key: readonly ["todos"];
    page: {
        items: readonly Todo[];
        nextCursor: null;
    };
    pageParam: number;
}>>
todosQuery
);
}} > <React.JSX.IntrinsicElements.input: React.DetailedHTMLProps<React.InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>input React.InputHTMLAttributes<HTMLInputElement>.name?: string | undefinedname="title" /> <React.JSX.IntrinsicElements.button: React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>button>Add</React.JSX.IntrinsicElements.button: React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>button> </React.JSX.IntrinsicElements.form: React.DetailedHTMLProps<React.FormHTMLAttributes<HTMLFormElement>, HTMLFormElement>form> ); }

await qc.invalidate(...) resolves once every query being read has refetched, so the form stays pending until the new todo is on screen. See Mutations for optimistic updates and errors.

What you get for free

Out of the box the cache dedupes requests, retries failures, refetches stale data when the window regains focus, garbage-collects unused entries and keeps unchanged data referentially stable. Read Important defaults next: they explain when raf-query fetches and when it doesn't.