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
useQueryanduse()
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.
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:
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.
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:
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
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:
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.