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

Create a query client, define a post and its paged comments, start them in a TanStack Start loader, read them with useQuery, useInfiniteQuery and use(), and add a comment.

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 client per router with createQueryClient. On the server, getRouter runs once per request, so each request gets its own cache; in the browser it runs once per tab.

src/query-client.ts
import { 
function createQueryClient<const TLinks extends ReadonlyArray<AnyLink> = [], const TFeatures extends ReadonlyArray<AnyFeature> = []>(config?: QueryClientConfig & {
    readonly features?: TFeatures;
    readonly links?: TLinks;
}): QueryClient<{
    readonly meta: MetaOf<[...TLinks, ...TFeatures]>;
}>

Create a query client. Behaviors are opt-in: pass links and features, or spread recommended(). The client's meta type is what they declare.

createQueryClient
, function recommended(): Recommended

The behaviors most apps want: retries, refetching on focus and reconnect, and polling. Spread it into the config:

recommended
} from "@raf-query/react";
export function
function makeQueryClient(): QueryClient<{
    readonly meta: Flat<Present<FocusRefetchMeta> & Present<ReconnectRefetchMeta> & Present<PollingMeta> & Present<RetryMeta>>;
}>
makeQueryClient
() {
return
createQueryClient<readonly [Link<{
    readonly meta: RetryMeta;
}>], readonly [Feature<{
    readonly meta: FocusRefetchMeta;
}>, Feature<{
    readonly meta: ReconnectRefetchMeta;
}>, Feature<{
    readonly meta: PollingMeta;
}>]>(config?: QueryClientConfig & {
    readonly features?: readonly [Feature<{
        readonly meta: FocusRefetchMeta;
    }>, Feature<{
        readonly meta: ReconnectRefetchMeta;
    }>, Feature<...>] | undefined;
    readonly links?: readonly [...] | undefined;
}): QueryClient<...>

Create a query client. Behaviors are opt-in: pass links and features, or spread recommended(). The client's meta type is what they declare.

createQueryClient
({ ...function recommended(): Recommended

The behaviors most apps want: retries, refetching on focus and reconnect, and polling. Spread it into the config:

recommended
(), QueryClientConfig.staleTime?: number | undefined

Default staleTime: 0.

staleTime
: 30_000 });
}
src/router.tsx
import { function QueryDevtools(props: QueryDevtoolsProps): React.JSX.Element

A toggle button that opens the devtools panel over the page.

QueryDevtools
} from "@raf-query/react";
import { function setupRouterIntegration<TRouter extends AnyRouter>(options: RouterIntegrationOptions<TRouter>): void

Connects a query client to a TanStack Router (and TanStack Start) router:

setupRouterIntegration
} from "@raf-query/react/tanstack-router";
import { const createRouter: CreateRouterFn

Creates a new Router instance for React.

@paramoptions — Router options used to configure the router.@returnsA Router instance to be provided to RouterProvider.
createRouter
} from "@tanstack/react-router";
import {
function Providers(props: {
    readonly children: React.ReactNode;
    readonly client?: QueryClient;
}): React.JSX.Element

The app's query client: the router's when it makes one, or one created here, per server render and per browser tab.

Providers
} from "./providers.tsx";
import {
function makeQueryClient(): QueryClient<{
    readonly meta: Flat<Present<FocusRefetchMeta> & Present<ReconnectRefetchMeta> & Present<PollingMeta> & Present<RetryMeta>>;
}>
makeQueryClient
} from "./query-client.ts";
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<{
    readonly meta: Flat<Present<FocusRefetchMeta> & Present<ReconnectRefetchMeta> & Present<PollingMeta> & Present<RetryMeta>>;
}>
queryClient
=
function makeQueryClient(): QueryClient<{
    readonly meta: Flat<Present<FocusRefetchMeta> & Present<ReconnectRefetchMeta> & Present<PollingMeta> & Present<RetryMeta>>;
}>
makeQueryClient
();
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, ... 6 more ..., undefined>, "never", false, RouterHistory, Record<...>>): RouterCore<...>

Creates a new Router instance for React.

@paramoptions — Router options used to configure the router.@returnsA Router instance to be provided to RouterProvider.
createRouter
({
context: {
    queryClient: QueryClient;
}
context
: { queryClient: QueryClient<any>queryClient },
defaultPreload?: false | "intent" | "viewport" | "render" | undefined

If false, routes will not be preloaded by default in any way.

defaultPreload
: "intent",
// The query cache decides what's fresh; loaders always run and dedupe there. defaultPreloadStaleTime?: number | undefined

The default preloadStaleTime a route should use if no preloadStaleTime is provided.

defaultPreloadStaleTime
: 0,
routeTree?: Route<Register, any, "/", "/", string, "__root__", undefined, {}, {
    queryClient: QueryClient;
}, AnyContext, AnyContext, {}, undefined, RootRouteChildren, FileRouteTypes, unknown, unknown, undefined> | undefined

The route tree that will be used to configure the router instance.

routeTree
,
scrollRestoration?: boolean | ((opts: {
    location: ParsedLocation;
}) => boolean) | undefined

If true, scroll restoration will be enabled

scrollRestoration
: true,
Wrap?: ((props: {
    children: any;
}) => React.JSX.Element) | undefined

A component that will be used to wrap the entire router.

Wrap
: (
props: {
    children: any;
}
props
) => (
<
function Providers(props: {
    readonly children: React.ReactNode;
    readonly client?: QueryClient;
}): React.JSX.Element

The app's query client: the router's when it makes one, or one created here, per server render and per browser tab.

Providers
client?: QueryClient<any> | undefinedclient={
const queryClient: QueryClient<{
    readonly meta: Flat<Present<FocusRefetchMeta> & Present<ReconnectRefetchMeta> & Present<PollingMeta> & Present<RetryMeta>>;
}>
queryClient
}>
{
props: {
    children: any;
}
props
.children: anychildren}
{import.meta.ImportMeta.env: ImportMetaEnvenv.ImportMetaEnv.DEV: booleanDEV ? <function QueryDevtools(props: QueryDevtoolsProps): React.JSX.Element

A toggle button that opens the devtools panel over the page.

QueryDevtools
/> : null}
</
function Providers(props: {
    readonly children: React.ReactNode;
    readonly client?: QueryClient;
}): React.JSX.Element

The app's query client: the router's when it makes one, or one created here, per server render and per browser tab.

Providers
>
), });
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;
}, ... 8 more ..., undefined>, "never", false, RouterHistory, Record<...>>>): void

Connects a query client to a TanStack Router (and TanStack Start) router:

setupRouterIntegration
({ RouterIntegrationOptions<TRouter extends AnyRouter>.queryClient: QueryClient<any>queryClient,
RouterIntegrationOptions<RouterCore<Route<Register, any, "/", "/", string, "__root__", undefined, {}, { queryClient: QueryClient; }, AnyContext, AnyContext, ... 6 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
;
}

Providers hands the router's client to the components below it with QueryClientProvider. The same component works in every framework:

src/providers.tsx
"use client";

import { type interface QueryClient<TTypes extends RootTypes = any>

A query cache. TTypes is its root's types, which factories built with bindQueryOptions<typeof makeQueryClient>() are checked against. The bare QueryClient type takes a client of any root.

QueryClient
,
function QueryClientProvider(props: {
    readonly children?: ReactNode;
    readonly client: QueryClient;
}): React.JSX.Element

Provides a client to the tree, and runs its features (focus and reconnect refetching, polling, …) while mounted.

QueryClientProvider
} from "@raf-query/react";
import { type type ReactNode = string | number | bigint | boolean | React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | Iterable<ReactNode> | React.ReactPortal | Promise<AwaitedReactNode> | null | undefined

Represents all of the things React can render.

ReactNode
, function useState<S>(initialState: S | (() => S)): [S, React.Dispatch<React.SetStateAction<S>>] (+1 overload)

Returns a stateful value, and a function to update it.

useState
} from "react";
import {
function makeQueryClient(): QueryClient<{
    readonly meta: Flat<Present<FocusRefetchMeta> & Present<ReconnectRefetchMeta> & Present<PollingMeta> & Present<RetryMeta>>;
}>
makeQueryClient
} from "~/query-client.ts";
/** * The app's query client: the router's when it makes one, or one created * here, per server render and per browser tab. */ export function
function Providers(props: {
    readonly children: ReactNode;
    readonly client?: QueryClient;
}): React.JSX.Element

The app's query client: the router's when it makes one, or one created here, per server render and per browser tab.

Providers
(
props: {
    readonly children: ReactNode;
    readonly client?: QueryClient;
}
props
: {
readonly children: ReactNodechildren: type ReactNode = string | number | bigint | boolean | React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | Iterable<ReactNode> | React.ReactPortal | Promise<AwaitedReactNode> | null | undefined

Represents all of the things React can render.

ReactNode
;
readonly client?: QueryClient<any> | undefinedclient?: interface QueryClient<TTypes extends RootTypes = any>

A query cache. TTypes is its root's types, which factories built with bindQueryOptions<typeof makeQueryClient>() are checked against. The bare QueryClient type takes a client of any root.

QueryClient
;
}) { const [
const client: QueryClient<any> | QueryClient<{
    readonly meta: Flat<Present<FocusRefetchMeta> & Present<ReconnectRefetchMeta> & Present<PollingMeta> & Present<RetryMeta>>;
}>
client
] =
useState<QueryClient<any> | QueryClient<{
    readonly meta: Flat<Present<FocusRefetchMeta> & Present<ReconnectRefetchMeta> & Present<PollingMeta> & Present<RetryMeta>>;
}>>(initialState: QueryClient<any> | QueryClient<{
    readonly meta: Flat<Present<FocusRefetchMeta> & Present<ReconnectRefetchMeta> & Present<PollingMeta> & Present<RetryMeta>>;
}> | (() => QueryClient<...> | QueryClient<...>)): [...] (+1 overload)

Returns a stateful value, and a function to update it.

useState
(() =>
props: {
    readonly children: ReactNode;
    readonly client?: QueryClient;
}
props
.client?: QueryClient<any> | undefinedclient ??
function makeQueryClient(): QueryClient<{
    readonly meta: Flat<Present<FocusRefetchMeta> & Present<ReconnectRefetchMeta> & Present<PollingMeta> & Present<RetryMeta>>;
}>
makeQueryClient
());
return ( <
function QueryClientProvider(props: {
    readonly children?: ReactNode;
    readonly client: QueryClient;
}): React.JSX.Element

Provides a client to the tree, and runs its features (focus and reconnect refetching, polling, …) while mounted.

QueryClientProvider
client: QueryClient<any>client={
const client: QueryClient<any> | QueryClient<{
    readonly meta: Flat<Present<FocusRefetchMeta> & Present<ReconnectRefetchMeta> & Present<PollingMeta> & Present<RetryMeta>>;
}>
client
}>{
props: {
    readonly children: ReactNode;
    readonly client?: QueryClient;
}
props
.children: ReactNodechildren}</
function QueryClientProvider(props: {
    readonly children?: ReactNode;
    readonly client: QueryClient;
}): React.JSX.Element

Provides a client to the tree, and runs its features (focus and reconnect refetching, polling, …) while mounted.

QueryClientProvider
>
); }

The client does nothing on its own. recommended() turns on what most apps want: retries, refetching stale data on window focus and reconnect, polling, and refetching after actions. staleTime keeps data fresh for 30 seconds, so navigating back to a page doesn't refetch it.

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 { interface QueryClient<TTypes extends RootTypes = any>

A query cache. TTypes is its root's types, which factories built with bindQueryOptions<typeof makeQueryClient>() are checked against. The bare QueryClient type takes a client of any root.

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>

Creates a root route factory that requires a router context type.

@returnsA factory function to configure and return a root route.
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<...>

Creates a root route factory that requires a router context type.

@returnsA factory function to configure and return a root route.
createRootRouteWithContext
<{ queryClient: QueryClient<any>queryClient: interface QueryClient<TTypes extends RootTypes = any>

A query cache. TTypes is its root's types, which factories built with bindQueryOptions<typeof makeQueryClient>() are checked against. The bare QueryClient type takes a client of any root.

QueryClient
}>()(
{}, );

2. Define the queries

The page is a blog post with its comments. A query is a factory: a function of at most one argument that returns queryOptions(...), or infiniteQueryOptions(...) for a list that loads page by page, like the comments. The argument is the query's input, here the post's slug. Loaders and components call the same factory, so the data's type flows 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";
import { type CommentPage,
function listComments(input: {
    cursor: number;
    slug: string;
}): Promise<CommentPage>
listComments
} from "./api.ts";
import {
const getPost: RequiredFetcher<undefined, ZodObject<{
    slug: ZodString;
}, $strip>, Promise<Post>>
getPost
} from "./posts.functions.ts";
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");
// A post's comments are a group inside `posts`. export const const comments: Group<readonly ["posts", "comments"]>comments = group<readonly ["posts"], "comments">(parent: Group<readonly ["posts"]>, name: "comments"): Group<readonly ["posts", "comments"]> (+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 posts: Group<readonly ["posts"]>posts, "comments");
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(opts: RequiredFetcherDataOptions<undefined, ZodObject<{
    slug: ZodString;
}, $strip>>): Promise<Post>
getPost
({
RequiredFetcherDataOptions<undefined, ZodObject<{ slug: ZodString; }, $strip>>.data: {
    slug: string;
}
data
:
input: {
    slug: string;
}
input
}),
}); } 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 comments: Group
<readonly [string]>(rest_0: string) => readonly ["posts", "comments", string]
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
({ cursor: numbercursor: context: InfiniteQueryFunctionContext<readonly ["posts", "comments", string], number>context.InfiniteQueryFunctionContext<readonly ["posts", "comments", string], number>.pageParam: numberpageParam, slug: stringslug:
input: {
    slug: string;
}
input
.slug: stringslug }),
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: CommentPage) => lastPage: CommentPagelastPage.CommentPage.nextCursor: number | nullnextCursor, }); }

The keys come from groups. comments is a group inside posts, so one call can invalidate every post query and every comment list at once, or only the comments.

listComments returns a page of comments and the cursor of the next one, null after the last page. getNextPageParam hands that cursor to the next request, as in TanStack Query.

3. Start them in the loader

The loader starts both queries in one load and returns their handles, small { queryKey, input } objects:

src/routes/posts.$slug.tsx
export const 
const Route: Route<Register, Route<Register, RootRoute<Register, undefined, {
    queryClient: QueryClient;
}, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/posts", "/posts", "/posts", "/posts", ZodObject<{
    sort: ZodDefault<ZodEnum<{
        newest: "newest";
        oldest: "oldest";
    }>>;
    page: ZodDefault<ZodInt>;
}, $strip>, ResolveParams<"/posts">, AnyContext, AnyContext, ... 7 more ..., undefined>, ... 15 more ..., undefined>
Route
=
createFileRoute<"/posts/$slug", Route<Register, RootRoute<Register, undefined, {
    queryClient: QueryClient;
}, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/posts", "/posts", "/posts", "/posts", ZodObject<{
    sort: ZodDefault<ZodEnum<{
        newest: "newest";
        oldest: "oldest";
    }>>;
    page: ZodDefault<ZodInt>;
}, $strip>, ResolveParams<"/posts">, AnyContext, AnyContext, ... 7 more ..., undefined>, "/posts/$slug", "/$slug", "/posts/$slug">(path?: "/posts/$slug" | undefined): <TRegister, TSearchValidator, TParams, TRouteContextFn, TBeforeLoadFn, TLoaderDeps, TLoaderFn, TChildren, TSSR, TMiddlewares, THandlers>(options?: (ParamsOptions<...> & ... 1 more ... & UpdatableRouteOptions<...>) | undefined) => Route<...>

Creates a file-based Route factory for a given path.

@parampath — File path literal for the route (usually auto-generated).@returnsA function that accepts Route options and returns a Route instance.
createFileRoute
("/posts/$slug")({
FilebaseRouteOptionsInterface<Register, Route<Register, RootRoute<Register, undefined, { queryClient: QueryClient; }, AnyContext, AnyContext, ... 6 more ..., undefined>, ... 15 more ..., undefined>, ... 12 more ..., undefined>.loader?: Constrain<(args: LoaderFnContext<Register, Route<Register, RootRoute<Register, undefined, {
    queryClient: QueryClient;
}, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/posts", "/posts", "/posts", "/posts", ZodObject<{
    sort: ZodDefault<ZodEnum<{
        newest: "newest";
        oldest: "oldest";
    }>>;
    page: ZodDefault<ZodInt>;
}, $strip>, ResolveParams<"/posts">, AnyContext, ... 8 more ..., undefined>, ... 7 more ..., undefined>) => Promise<...>, RouteLoaderFn<...> | RouteLoaderObject<...>> | undefined
loader
: async (
args: LoaderFnContext<Register, Route<Register, RootRoute<Register, undefined, {
    queryClient: QueryClient;
}, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/posts", "/posts", "/posts", "/posts", ZodObject<{
    sort: ZodDefault<ZodEnum<{
        newest: "newest";
        oldest: "oldest";
    }>>;
    page: ZodDefault<ZodInt>;
}, $strip>, ResolveParams<"/posts">, AnyContext, AnyContext, ... 7 more ..., undefined>, ... 7 more ..., undefined>
args
) => {
const { const slug: stringslug } =
args: LoaderFnContext<Register, Route<Register, RootRoute<Register, undefined, {
    queryClient: QueryClient;
}, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/posts", "/posts", "/posts", "/posts", ZodObject<{
    sort: ZodDefault<ZodEnum<{
        newest: "newest";
        oldest: "oldest";
    }>>;
    page: ZodDefault<ZodInt>;
}, $strip>, ResolveParams<"/posts">, AnyContext, AnyContext, ... 7 more ..., undefined>, ... 7 more ..., undefined>
args
.
LoaderFnContext<Register, Route<Register, RootRoute<Register, undefined, { queryClient: QueryClient; }, AnyContext, AnyContext, ... 6 more ..., undefined>, ... 15 more ..., undefined>, ... 7 more ..., undefined>.params: {
    slug: string;
}
params
;
const [
const handles: {
    readonly post: QueryHandle<(input: {
        slug: string;
    }) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>;
    readonly comments: QueryHandle<(input: {
        slug: string;
    }) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>;
}
handles
] = await
load<{
    readonly post: LoadEntry<(input: {
        slug: string;
    }) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>, "swr">;
    readonly comments: LoadEntry<(input: {
        slug: string;
    }) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>, "prefetch">;
}>(client: QueryClient, entries: {
    readonly post: LoadEntry<(input: {
        slug: string;
    }) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>, "swr">;
    readonly comments: LoadEntry<(input: {
        slug: string;
    }) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, 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
(
args: LoaderFnContext<Register, Route<Register, RootRoute<Register, undefined, {
    queryClient: QueryClient;
}, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/posts", "/posts", "/posts", "/posts", ZodObject<{
    sort: ZodDefault<ZodEnum<{
        newest: "newest";
        oldest: "oldest";
    }>>;
    page: ZodDefault<ZodInt>;
}, $strip>, ResolveParams<"/posts">, AnyContext, AnyContext, ... 7 more ..., undefined>, ... 7 more ..., undefined>
args
.
LoaderFnContext<Register, Route<Register, RootRoute<Register, undefined, { queryClient: QueryClient; }, AnyContext, AnyContext, ... 6 more ..., undefined>, ... 15 more ..., undefined>, ... 7 more ..., undefined>.context: {
    queryClient: QueryClient;
}
context
.queryClient: QueryClient<any>queryClient, {
// Waits only if the post isn't in the cache yet.
post: LoadEntry<(input: {
    slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>, "swr">
post
:
swr<(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>, "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
(
function postQuery(input: {
    slug: string;
}): QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>
postQuery
, { slug: stringslug }),
// Never waits: the comments load while the page renders.
comments: LoadEntry<(input: {
    slug: string;
}) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>, "prefetch">
comments
:
prefetch<(input: {
    slug: string;
}) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>(factory: (input: {
    slug: string;
}) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>, ...args: LoadEntryArgs<(input: {
    slug: string;
}) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, 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 commentsQuery(input: {
    slug: string;
}): InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>
commentsQuery
, { slug: stringslug }),
}); return
const handles: {
    readonly post: QueryHandle<(input: {
        slug: string;
    }) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>;
    readonly comments: QueryHandle<(input: {
        slug: string;
    }) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>;
}
handles
;
}, UpdatableRouteOptionsExtensions.component?: RouteComponent | undefinedcomponent: function PostPage(): React.JSX.ElementPostPage, });

swr waits for the post when the cache doesn't have it yet, because the page can't lay out without it. prefetch starts the comments without waiting, so the page renders while they're on their way. Starting queries covers the trade-off.

4. Read them in components

src/routes/posts.$slug.tsx
function function PostPage(): React.JSX.ElementPostPage() {
	const 
const data: {
    readonly post: QueryHandle<(input: {
        slug: string;
    }) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>;
    readonly comments: QueryHandle<(input: {
        slug: string;
    }) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>;
}
data
=
const Route: Route<Register, Route<Register, RootRoute<Register, undefined, {
    queryClient: QueryClient;
}, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/posts", "/posts", "/posts", "/posts", ZodObject<{
    sort: ZodDefault<ZodEnum<{
        newest: "newest";
        oldest: "oldest";
    }>>;
    page: ZodDefault<ZodInt>;
}, $strip>, ResolveParams<"/posts">, AnyContext, AnyContext, ... 7 more ..., undefined>, ... 15 more ..., undefined>
Route
.
RouteExtensions<"/posts/$slug", "/posts/$slug">.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<...>>, "/posts/$slug", true, unknown, boolean> & OptionalStructuralSharing<...>) | undefined) => {
    ...;
}
useLoaderData
();
// The route already waited for this, so this is the post itself. const [
const postThenable: QueryValue<(input: {
    slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>
postThenable
] =
useQuery<(input: {
    slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>(factory: (input: {
    slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>, handle: QueryHandle<(input: {
    slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>, options?: UseQueryOptions): readonly [...]

Read a query by its handle.

useQuery
(
function postQuery(input: {
    slug: string;
}): QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>
postQuery
,
const data: {
    readonly post: QueryHandle<(input: {
        slug: string;
    }) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>;
    readonly comments: QueryHandle<(input: {
        slug: string;
    }) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>;
}
data
.
post: QueryHandle<(input: {
    slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>
post
);
const post = use<Post>(usable: React.Usable<Post>): Postuse(
const postThenable: QueryValue<(input: {
    slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>
postThenable
);
const post: Post
return ( <React.JSX.IntrinsicElements.article: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>article> <React.JSX.IntrinsicElements.h1: React.DetailedHTMLProps<React.HTMLAttributes<HTMLHeadingElement>, HTMLHeadingElement>h1>{const post: Postpost.Post.title: stringtitle}</React.JSX.IntrinsicElements.h1: React.DetailedHTMLProps<React.HTMLAttributes<HTMLHeadingElement>, HTMLHeadingElement>h1> <const Suspense: React.ExoticComponent<React.SuspenseProps>

Lets you display a fallback until its children have finished loading.

Suspense
React.SuspenseProps.fallback?: React.ReactNode

A fallback react tree to show when a Suspense child (like React.lazy) suspends

fallback
={<function CommentsSkeleton(): React.JSX.ElementCommentsSkeleton />}>
<
function Comments(props: {
    handle: QueryHandle<typeof commentsQuery>;
}): React.JSX.Element
Comments
handle: QueryHandle<(input: {
    slug: string;
}) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>
handle
={
const data: {
    readonly post: QueryHandle<(input: {
        slug: string;
    }) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>;
    readonly comments: QueryHandle<(input: {
        slug: string;
    }) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>;
}
data
.
comments: QueryHandle<(input: {
    slug: string;
}) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>
comments
} />
</const Suspense: React.ExoticComponent<React.SuspenseProps>

Lets you display a fallback until its children have finished loading.

Suspense
>
<
function AddComment(props: {
    slug: string;
}): React.JSX.Element
AddComment
slug: stringslug={const post: Postpost.Post.slug: stringslug} />
</React.JSX.IntrinsicElements.article: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>article> ); } function
function Comments(props: {
    handle: QueryHandle<typeof commentsQuery>;
}): React.JSX.Element
Comments
(
props: {
    handle: QueryHandle<typeof commentsQuery>;
}
props
: {
handle: QueryHandle<(input: {
    slug: string;
}) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>
handle
: 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 commentsQuery(input: {
    slug: string;
}): InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>
commentsQuery
> }) {
// May still be loading. If so, use() suspends until it lands. const [
const pagesThenable: QueryValue<(input: {
    slug: string;
}) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>
pagesThenable
,
const commentsState: InfiniteQueryState<(input: {
    slug: string;
}) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>
commentsState
] =
useInfiniteQuery<(input: {
    slug: string;
}) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>(factory: (input: {
    slug: string;
}) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>, handle: QueryHandle<(input: {
    slug: string;
}) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>, options?: UseQueryOptions): readonly [...]

Read a paged query (infiniteQueryOptions) by its handle. The data is { pages, pageParams }, and its state loads more pages.

useInfiniteQuery
(
function commentsQuery(input: {
    slug: string;
}): InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>
commentsQuery
,
props: {
    handle: QueryHandle<typeof commentsQuery>;
}
props
.
handle: QueryHandle<(input: {
    slug: string;
}) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>
handle
,
); const pages = use<InfiniteData<CommentPage, number>>(usable: React.Usable<InfiniteData<CommentPage, number>>): InfiniteData<CommentPage, number>use(
const pagesThenable: QueryValue<(input: {
    slug: string;
}) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>
pagesThenable
);
const pages: InfiniteData<CommentPage, number>
return ( <> {const pages: InfiniteData<CommentPage, number>pages.InfiniteData<CommentPage, number>.pages: readonly CommentPage[]pages.ReadonlyArray<CommentPage>.flatMap<PostComment, undefined>(callback: (this: undefined, value: CommentPage, index: number, array: CommentPage[]) => PostComment | readonly PostComment[], thisArg?: undefined): PostComment[]

Calls a defined callback function on each element of an array. Then, flattens the result into a new array. This is identical to a map followed by flat with depth 1.

@paramcallback — A function that accepts up to three arguments. The flatMap method calls the callback function one time for each element in the array.@paramthisArg — An object to which the this keyword can refer in the callback function. If thisArg is omitted, undefined is used as the this value.
flatMap
((page: CommentPagepage) => page: CommentPagepage.CommentPage.items: readonly PostComment[]items).Array<PostComment>.map<React.JSX.Element>(callbackfn: (value: PostComment, index: number, array: PostComment[]) => React.JSX.Element, thisArg?: any): React.JSX.Element[]

Calls a defined callback function on each element of an array, and returns an array that contains the results.

@paramcallbackfn — A function that accepts up to three arguments. The map method calls the callbackfn function one time for each element in the array.@paramthisArg — An object to which the this keyword can refer in the callbackfn function. If thisArg is omitted, undefined is used as the this value.
map
(function renderComment(comment: PostComment): React.JSX.ElementrenderComment)}
{
const commentsState: InfiniteQueryState<(input: {
    slug: string;
}) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>
commentsState
.PageControls.hasNextPage: booleanhasNextPage ? (
<React.JSX.IntrinsicElements.button: React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>button React.ButtonHTMLAttributes<HTMLButtonElement>.disabled?: boolean | undefineddisabled={
const commentsState: InfiniteQueryState<(input: {
    slug: string;
}) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>
commentsState
.PageControls.isFetchingNextPage: booleanisFetchingNextPage}
React.DOMAttributes<HTMLButtonElement>.onClick?: React.MouseEventHandler<HTMLButtonElement> | undefinedonClick={() =>
const commentsState: InfiniteQueryState<(input: {
    slug: string;
}) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>
commentsState
.PageControls.fetchNextPage(): Promise<void>

Load the page after the last one. Resolves when it's there.

fetchNextPage
()}
> {
const commentsState: InfiniteQueryState<(input: {
    slug: string;
}) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>
commentsState
.PageControls.isFetchingNextPage: booleanisFetchingNextPage ? "Loading…" : "Load more"}
</React.JSX.IntrinsicElements.button: React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>button> ) : null} </> ); }

useQuery takes the factory as well as the handle because the handle is plain data, { queryKey, input }, so it can cross from a loader or a server component to the browser, and a function can't: the handle can't carry the queryFn, so you pass the factory alongside it.

useQuery returns a stable thenable and the query's state. use() suspends until the data is there, then returns it. The loader already waited for the post, so the thenable is fulfilled and use() returns it without suspending.

The comments may still be loading, so <Comments> sits in its own Suspense boundary and the post renders without them. useInfiniteQuery reads them as { pages, pageParams }, and its state loads more: fetchNextPage() fetches the next page and appends it, hasNextPage says whether there is one, and isFetchingNextPage is true while it loads. See Infinite queries.

5. Write data with an Action

There is no useMutation. Adding a comment is a form action in the component: await the write, then invalidate what it changed with the client from useQueryClient():

src/routes/posts.$slug.tsx
function 
function AddComment(props: {
    slug: string;
}): React.JSX.Element
AddComment
(
props: {
    slug: string;
}
props
: { slug: stringslug: string }) {
const const queryClient: QueryClient<any>queryClient = function useQueryClient(): QueryClient

The client from the nearest QueryClientProvider.

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 addComment(input: {
    body: string;
    slug: string;
}): Promise<PostComment>
addComment
({
body: stringbody:
var String: StringConstructor
(value?: any) => string

Allows manipulation and formatting of text strings and determination and location of substrings within strings.

String
(formData: FormDataformData.FormData.get(name: string): FormDataEntryValue | null

The get() method of the FormData interface returns the first value associated with a given key from within a FormData object. If you expect multiple values and want all of them, use the getAll() method instead.

get
("body")),
slug: stringslug:
props: {
    slug: string;
}
props
.slug: stringslug,
}); await
invalidateMany<any, readonly [readonly [(input: {
    slug: string;
}) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>, {
    readonly slug: string;
}]]>(client: QueryClient<any>, targets: readonly [readonly [(input: {
    slug: string;
}) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>, {
    ...;
}]]): Promise<void> (+3 overloads)

Mark queries stale: a factory's (optionally only those whose input matches partial), a group's (keys that start with its path, whether the group built them or not), or those a target's where picks. Read queries refetch now; the rest when next read. Resolves when the refetches settle.

invalidateMany
(const queryClient: QueryClient<any>queryClient, [
[
function commentsQuery(input: {
    slug: string;
}): InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>
commentsQuery
, { slug?: string | undefinedslug:
props: {
    slug: string;
}
props
.slug: stringslug }],
]); }} > <React.JSX.IntrinsicElements.textarea: React.DetailedHTMLProps<React.TextareaHTMLAttributes<HTMLTextAreaElement>, HTMLTextAreaElement>textarea React.TextareaHTMLAttributes<HTMLTextAreaElement>.name?: string | undefinedname="body" React.TextareaHTMLAttributes<HTMLTextAreaElement>.required?: boolean | undefinedrequired /> <React.JSX.IntrinsicElements.button: React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>button>Add comment</React.JSX.IntrinsicElements.button: React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>button> </React.JSX.IntrinsicElements.form: React.DetailedHTMLProps<React.FormHTMLAttributes<HTMLFormElement>, HTMLFormElement>form> ); }

invalidateMany takes a list of targets. [commentsQuery, { slug }] is this post's comments, and the list refetches every page it has loaded. The verb resolves once that refetch has settled, so the form stays pending until the new comment is on screen. See Mutations for optimistic updates and errors.

What you get

The cache always dedupes requests, garbage-collects unused entries and keeps unchanged data referentially stable. With recommended(), it also retries failures and refetches stale data when the window regains focus. Read Important defaults next: they explain when raf-query fetches and when it doesn't.