Skip to content

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

raf-query
Early · 0.x on npm

Render-as-you-fetch data fetching for React

Queries start in route loaders and React Server Components, travel to components as small serializable handles, and are read with React’s use() under Suspense. The cache works like TanStack Query’s.

$ pnpm add @raf-query/react

src/routes/blog.$slug.tsx
export const 
const Route: Route<Register, RootRoute<Register, undefined, {
    queryClient: QueryClient;
}, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/blog/$slug", "/blog/$slug", "/blog/$slug", "/blog/$slug", undefined, ResolveParams<"/blog/$slug">, AnyContext, AnyContext, AnyContext, {}, (args: LoaderFnContext<Register, RootRoute<Register, undefined, {
    queryClient: QueryClient;
}, ... 8 more ..., undefined>, ... 7 more ..., undefined>) => Promise<...>, ... 4 more ..., undefined>
Route
=
createFileRoute<"/blog/$slug", RootRoute<Register, undefined, {
    queryClient: QueryClient;
}, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/blog/$slug", "/blog/$slug", "/blog/$slug">(path?: "/blog/$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
("/blog/$slug")({
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;
}, ... 8 more ..., undefined>, ... 7 more ..., undefined>) => Promise<...>, RouteLoaderFn<...> | RouteLoaderObject<...>> | undefined
loader
: async (
args: LoaderFnContext<Register, RootRoute<Register, undefined, {
    queryClient: QueryClient;
}, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/blog/$slug", ResolveParams<"/blog/$slug">, {}, AnyContext, AnyContext, AnyContext, unknown, undefined>
args
) => {
const { const slug: stringslug } =
args: LoaderFnContext<Register, RootRoute<Register, undefined, {
    queryClient: QueryClient;
}, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/blog/$slug", ResolveParams<"/blog/$slug">, {}, AnyContext, AnyContext, AnyContext, unknown, undefined>
args
.
LoaderFnContext<Register, RootRoute<Register, undefined, { queryClient: QueryClient; }, AnyContext, AnyContext, {}, ... 5 more ..., undefined>, ... 7 more ..., undefined>.params: {
    slug: string;
}
params
;
// Never waits: both load while the page renders. const [
const handles: {
    readonly comments: QueryHandle<(input: {
        slug: string;
    }) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>;
    readonly post: QueryHandle<(input: {
        slug: string;
    }) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>;
}
handles
] = await
load<{
    readonly comments: LoadEntry<(input: {
        slug: string;
    }) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>, "prefetch">;
    readonly post: LoadEntry<(input: {
        slug: string;
    }) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>, "prefetch">;
}>(client: QueryClient, entries: {
    readonly comments: LoadEntry<(input: {
        slug: string;
    }) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>, "prefetch">;
    readonly post: LoadEntry<(input: {
        slug: string;
    }) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, 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, RootRoute<Register, undefined, {
    queryClient: QueryClient;
}, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/blog/$slug", ResolveParams<"/blog/$slug">, {}, 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<any>queryClient, {
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 }),
post: LoadEntry<(input: {
    slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>, "prefetch">
post
:
prefetch<(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>, "prefetch">

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 postQuery(input: {
    slug: string;
}): QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>
postQuery
, { slug: stringslug }),
}); return
const handles: {
    readonly comments: QueryHandle<(input: {
        slug: string;
    }) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>;
    readonly post: QueryHandle<(input: {
        slug: string;
    }) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>;
}
handles
;
}, UpdatableRouteOptionsExtensions.component?: RouteComponent | undefinedcomponent: function PostPage(): React.JSX.ElementPostPage, }); function function PostPage(): React.JSX.ElementPostPage() { const
const data: {
    readonly comments: QueryHandle<(input: {
        slug: string;
    }) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>;
    readonly post: QueryHandle<(input: {
        slug: string;
    }) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>;
}
data
=
const Route: Route<Register, RootRoute<Register, undefined, {
    queryClient: QueryClient;
}, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/blog/$slug", "/blog/$slug", "/blog/$slug", "/blog/$slug", undefined, ResolveParams<"/blog/$slug">, AnyContext, AnyContext, AnyContext, {}, (args: LoaderFnContext<Register, RootRoute<Register, undefined, {
    queryClient: QueryClient;
}, ... 8 more ..., undefined>, ... 7 more ..., undefined>) => Promise<...>, ... 4 more ..., undefined>
Route
.
RouteExtensions<"/blog/$slug", "/blog/$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<...>>, "/blog/$slug", true, unknown, boolean> & OptionalStructuralSharing<...>) | undefined) => {
    ...;
}
useLoaderData
();
return ( <React.JSX.IntrinsicElements.article: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>article> <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 PostSkeleton(): React.ReactNodePostSkeleton />}>
<
function Post(props: {
    handle: QueryHandle<typeof postQuery>;
}): React.JSX.Element
Post
handle: QueryHandle<(input: {
    slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>
handle
={
const data: {
    readonly comments: QueryHandle<(input: {
        slug: string;
    }) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>;
    readonly post: QueryHandle<(input: {
        slug: string;
    }) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>;
}
data
.
post: QueryHandle<(input: {
    slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>
post
} />
</const Suspense: React.ExoticComponent<React.SuspenseProps>

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

Suspense
>
<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 PostComments(props: {
    handle: QueryHandle<typeof commentsQuery>;
}): React.JSX.Element[]
PostComments
handle: QueryHandle<(input: {
    slug: string;
}) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>
handle
={
const data: {
    readonly comments: QueryHandle<(input: {
        slug: string;
    }) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>;
    readonly post: QueryHandle<(input: {
        slug: string;
    }) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, 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
>
</React.JSX.IntrinsicElements.article: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>article> ); } function
function Post(props: {
    handle: QueryHandle<typeof postQuery>;
}): React.JSX.Element
Post
(
props: {
    handle: QueryHandle<typeof postQuery>;
}
props
: {
handle: QueryHandle<(input: {
    slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, 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 postQuery(input: {
    slug: string;
}): QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>
postQuery
> }) {
const [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
,
props: {
    handle: QueryHandle<typeof postQuery>;
}
props
.
handle: QueryHandle<(input: {
    slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>
handle
);
const postThenable: QueryValue<(input: {
    slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>
const const post: Postpost = use<Post>(usable: React.Usable<Post>): Postuse(
const postThenable: QueryValue<(input: {
    slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>
postThenable
);
return <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>; }
src/app/(blog)/posts/[slug]/page.tsx
export default async function 
function Page(props: {
    params: Promise<{
        slug: string;
    }>;
}): Promise<JSX.Element>
Page
(
props: {
    params: Promise<{
        slug: string;
    }>;
}
props
: {
params: Promise<{
    slug: string;
}>
params
: interface Promise<T>

Represents the completion of an asynchronous operation

Promise
<{ slug: stringslug: string }>;
}) { const { const slug: stringslug } = await
props: {
    params: Promise<{
        slug: string;
    }>;
}
props
.
params: Promise<{
    slug: string;
}>
params
;
const
const qc: QueryClient<{
    readonly meta: Flat<Present<FocusRefetchMeta> & Present<ReconnectRefetchMeta> & Present<PollingMeta> & Present<RetryMeta>>;
}>
qc
=
function getQueryClient(): QueryClient<{
    readonly meta: Flat<Present<FocusRefetchMeta> & Present<ReconnectRefetchMeta> & Present<PollingMeta> & Present<RetryMeta>>;
}>

The server components' client, one per request.

getQueryClient
();
// Never waits: both load while the page streams. const [
const handles: {
    readonly comments: QueryHandle<(input: {
        slug: string;
    }) => InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>;
    readonly post: QueryHandle<(input: {
        slug: string;
    }) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>>;
}
handles
] = await
load<{
    readonly comments: LoadEntry<(input: {
        slug: string;
    }) => InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>, "prefetch">;
    readonly post: LoadEntry<(input: {
        slug: string;
    }) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>, "prefetch">;
}>(client: QueryClient, entries: {
    readonly comments: LoadEntry<(input: {
        slug: string;
    }) => InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>, "prefetch">;
    readonly post: LoadEntry<(input: {
        slug: string;
    }) => QueryOptions<readonly ["post", string], Post, Post, 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
(
const qc: QueryClient<{
    readonly meta: Flat<Present<FocusRefetchMeta> & Present<ReconnectRefetchMeta> & Present<PollingMeta> & Present<RetryMeta>>;
}>
qc
, {
comments: LoadEntry<(input: {
    slug: string;
}) => InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>, "prefetch">
comments
:
prefetch<(input: {
    slug: string;
}) => InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>(factory: (input: {
    slug: string;
}) => InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>, ...args: LoadEntryArgs<(input: {
    slug: string;
}) => InfiniteQueryOptions<readonly ["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 ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>
commentsQuery
, { slug: stringslug }),
post: LoadEntry<(input: {
    slug: string;
}) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>, "prefetch">
post
:
prefetch<(input: {
    slug: string;
}) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>>(factory: (input: {
    slug: string;
}) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>, ...args: LoadEntryArgs<(input: {
    slug: string;
}) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>>): LoadEntry<(input: {
    slug: string;
}) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>, "prefetch">

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 postQuery(input: {
    slug: string;
}): QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>
postQuery
, { slug: stringslug }),
}); return ( <function HydrationBoundary(props: HydrationBoundaryProps): JSX.Element

Writes server data into the browser (or SSR) client.

HydrationBoundary
client: QueryClient<any>

The request's server client (e.g. from React's cache()). Server components only.

client
={
const qc: QueryClient<{
    readonly meta: Flat<Present<FocusRefetchMeta> & Present<ReconnectRefetchMeta> & Present<PollingMeta> & Present<RetryMeta>>;
}>
qc
}>
<JSX.IntrinsicElements.article: DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>article> <const Suspense: ExoticComponent<SuspenseProps>

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

Suspense
SuspenseProps.fallback?: ReactNode

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

fallback
={<function PostSkeleton(): JSX.ElementPostSkeleton />}>
<
function Post(props: {
    handle: QueryHandle<typeof postQuery>;
}): JSX.Element
Post
handle: QueryHandle<(input: {
    slug: string;
}) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>>
handle
={
const handles: {
    readonly comments: QueryHandle<(input: {
        slug: string;
    }) => InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>;
    readonly post: QueryHandle<(input: {
        slug: string;
    }) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>>;
}
handles
.
post: QueryHandle<(input: {
    slug: string;
}) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>>
post
} />
</const Suspense: ExoticComponent<SuspenseProps>

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

Suspense
>
<const Suspense: ExoticComponent<SuspenseProps>

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

Suspense
SuspenseProps.fallback?: ReactNode

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

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

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

Suspense
>
</JSX.IntrinsicElements.article: DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>article> </function HydrationBoundary(props: HydrationBoundaryProps): JSX.Element

Writes server data into the browser (or SSR) client.

HydrationBoundary
>
); }
src/app/(blog)/components/post.tsx
"use client";
export function 
function Post(props: {
    handle: QueryHandle<typeof postQuery>;
}): JSX.Element
Post
(
props: {
    handle: QueryHandle<typeof postQuery>;
}
props
: {
handle: QueryHandle<(input: {
    slug: string;
}) => QueryOptions<readonly ["post", string], Post, Post, 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 postQuery(input: {
    slug: string;
}): QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>
postQuery
> }) {
const [postThenable] =
useQuery<(input: {
    slug: string;
}) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>>(factory: (input: {
    slug: string;
}) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>, handle: QueryHandle<(input: {
    slug: string;
}) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>>, options?: UseQueryOptions): readonly [QueryValue<(input: {
    slug: string;
}) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>>, QueryState<...>]

Read a query by its handle.

useQuery
(
function postQuery(input: {
    slug: string;
}): QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>
postQuery
,
props: {
    handle: QueryHandle<typeof postQuery>;
}
props
.
handle: QueryHandle<(input: {
    slug: string;
}) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>>
handle
);
const postThenable: QueryValue<(input: {
    slug: string;
}) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>>
const const post: Postpost = use<Post>(usable: Usable<Post>): Postuse(
const postThenable: QueryValue<(input: {
    slug: string;
}) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>>
postThenable
);
return <JSX.IntrinsicElements.h1: DetailedHTMLProps<HTMLAttributes<HTMLHeadingElement>, HTMLHeadingElement>h1>{const post: Postpost.Post.title: stringtitle}</JSX.IntrinsicElements.h1: DetailedHTMLProps<HTMLAttributes<HTMLHeadingElement>, HTMLHeadingElement>h1>; }
src/app/(blog)/components/comments.tsx
app/routes/blog.$slug.tsx
export async function 
function loader(args: Route.LoaderArgs): Promise<{
    dehydratedState: DehydratedState;
    comments: QueryHandle<(input: {
        slug: string;
    }) => InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>;
    post: QueryHandle<(input: {
        slug: string;
    }) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>>;
}>
loader
(
args: CreateServerLoaderArgs<Info & {
    module: typeof import("./blog.$slug");
    matches: Matches;
}>
args
: Route.
type Route.LoaderArgs = {
    request: Request;
    url: URL;
    params: {
        slug: string;
    };
    pattern: string;
    context: Readonly<RouterContextProvider>;
}
LoaderArgs
) {
const const qc: QueryClient<any>qc =
args: CreateServerLoaderArgs<Info & {
    module: typeof import("./blog.$slug");
    matches: Matches;
}>
args
.context: Readonly<RouterContextProvider>

An instance of RouterContextProvider that can be used for type-safe access to context values set in your route middlewares. If you are using a custom server adapter, you may provide an initial set of context values from your getLoadContext function.

context
.get<QueryClient<any>>(context: RouterContext<QueryClient<any>>): QueryClient<any>

Access a value from the context. If no value has been set for the context, it will return the context's defaultValue if provided, or throw an error if no defaultValue was set.

@paramcontext — The context to get the value for@returnsThe value for the context, or the context's defaultValue if no value was set
get
(const queryClientContext: RouterContext<QueryClient<any>>queryClientContext);
const { const slug: stringslug } =
args: CreateServerLoaderArgs<Info & {
    module: typeof import("./blog.$slug");
    matches: Matches;
}>
args
.
params: {
    slug: string;
}

Dynamic route params for the current route.

params
;
// Never waits: both load while the page renders. const [
const handles: {
    readonly comments: QueryHandle<(input: {
        slug: string;
    }) => InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>;
    readonly post: QueryHandle<(input: {
        slug: string;
    }) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>>;
}
handles
] = await
load<{
    readonly comments: LoadEntry<(input: {
        slug: string;
    }) => InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>, "prefetch">;
    readonly post: LoadEntry<(input: {
        slug: string;
    }) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>, "prefetch">;
}>(client: QueryClient, entries: {
    readonly comments: LoadEntry<(input: {
        slug: string;
    }) => InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>, "prefetch">;
    readonly post: LoadEntry<(input: {
        slug: string;
    }) => QueryOptions<readonly ["post", string], Post, Post, 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
(const qc: QueryClient<any>qc, {
comments: LoadEntry<(input: {
    slug: string;
}) => InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>, "prefetch">
comments
:
prefetch<(input: {
    slug: string;
}) => InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>(factory: (input: {
    slug: string;
}) => InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>, ...args: LoadEntryArgs<(input: {
    slug: string;
}) => InfiniteQueryOptions<readonly ["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 ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>
commentsQuery
, { slug: stringslug }),
post: LoadEntry<(input: {
    slug: string;
}) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>, "prefetch">
post
:
prefetch<(input: {
    slug: string;
}) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>>(factory: (input: {
    slug: string;
}) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>, ...args: LoadEntryArgs<(input: {
    slug: string;
}) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>>): LoadEntry<(input: {
    slug: string;
}) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>, "prefetch">

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 postQuery(input: {
    slug: string;
}): QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>
postQuery
, { slug: stringslug }),
}); return { ...
const handles: {
    readonly comments: QueryHandle<(input: {
        slug: string;
    }) => InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>;
    readonly post: QueryHandle<(input: {
        slug: string;
    }) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>>;
}
handles
, dehydratedState: DehydratedStatedehydratedState: function dehydrate(client: QueryClient, options?: DehydrateOptions): DehydratedState

Snapshot the client's entries for transfer.

dehydrate
(const qc: QueryClient<any>qc) };
} export default function function PostPage(props: Route.ComponentProps): React.JSX.ElementPostPage(
props: CreateComponentProps<Info & {
    module: typeof import("./blog.$slug");
    matches: Matches;
}, false>
props
: Route.
type Route.ComponentProps = {
    params: {
        slug: string;
    };
    matches: [{
        id: "root";
        params: Record<string, string | undefined>;
        pathname: string;
        loaderData: undefined;
        handle: unknown;
    }, {
        id: "routes/blog.$slug";
        params: Record<string, string | undefined>;
        pathname: string;
        loaderData: {
            dehydratedState: {
                readonly entries: readonly {
                    readonly data?: undefined;
                    readonly promise?: Promise<{
                        readonly data?: undefined;
                        readonly updatedAt: number;
                    }> | undefined;
                    readonly queryKey: readonly undefined[];
                    readonly updatedAt: number;
                }[];
            };
            comments: {
                readonly input: {
                    slug: string;
                };
                readonly queryKey: ["comments", string];
            };
            post: {
                ...;
            };
        };
        handle: unknown;
    }, ...({
        ...;
    } | undefined)[]];
} & {
    ...;
}
ComponentProps
) {
const {
const comments: {
    readonly input: {
        slug: string;
    };
    readonly queryKey: ["comments", string];
}
comments
,
const dehydratedState: {
    readonly entries: readonly {
        readonly data?: undefined;
        readonly promise?: Promise<{
            readonly data?: undefined;
            readonly updatedAt: number;
        }> | undefined;
        readonly queryKey: readonly undefined[];
        readonly updatedAt: number;
    }[];
}
dehydratedState
,
const post: {
    readonly input: {
        slug: string;
    };
    readonly queryKey: ["post", string];
}
post
} =
props: CreateComponentProps<Info & {
    module: typeof import("./blog.$slug");
    matches: Matches;
}, false>
props
.
loaderData: {
    dehydratedState: {
        readonly entries: readonly {
            readonly data?: undefined;
            readonly promise?: Promise<{
                readonly data?: undefined;
                readonly updatedAt: number;
            }> | undefined;
            readonly queryKey: readonly undefined[];
            readonly updatedAt: number;
        }[];
    };
    comments: {
        readonly input: {
            slug: string;
        };
        readonly queryKey: ["comments", string];
    };
    post: {
        ...;
    };
}

The data returned from the loader or clientLoader

loaderData
;
return ( <function HydrationBoundary(props: HydrationBoundaryProps): React.JSX.Element

Writes server data into the browser (or SSR) client.

HydrationBoundary
state: DehydratedState

A loader's dehydrate(client).

state
={
const dehydratedState: {
    readonly entries: readonly {
        readonly data?: undefined;
        readonly promise?: Promise<{
            readonly data?: undefined;
            readonly updatedAt: number;
        }> | undefined;
        readonly queryKey: readonly undefined[];
        readonly updatedAt: number;
    }[];
}
dehydratedState
}>
<React.JSX.IntrinsicElements.article: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>article> <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 PostSkeleton(): React.ReactNodePostSkeleton />}>
<
function Post(props: {
    handle: QueryHandle<typeof postQuery>;
}): React.JSX.Element
Post
handle: QueryHandle<(input: {
    slug: string;
}) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>>
handle
={
const post: {
    readonly input: {
        slug: string;
    };
    readonly queryKey: ["post", string];
}
post
} />
</const Suspense: React.ExoticComponent<React.SuspenseProps>

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

Suspense
>
<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 PostComments(props: {
    handle: QueryHandle<typeof commentsQuery>;
}): React.JSX.Element[]
PostComments
handle: QueryHandle<(input: {
    slug: string;
}) => InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>
handle
={
const comments: {
    readonly input: {
        slug: string;
    };
    readonly queryKey: ["comments", string];
}
comments
} />
</const Suspense: React.ExoticComponent<React.SuspenseProps>

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

Suspense
>
</React.JSX.IntrinsicElements.article: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>article> </function HydrationBoundary(props: HydrationBoundaryProps): React.JSX.Element

Writes server data into the browser (or SSR) client.

HydrationBoundary
>
); } function
function Post(props: {
    handle: QueryHandle<typeof postQuery>;
}): React.JSX.Element
Post
(
props: {
    handle: QueryHandle<typeof postQuery>;
}
props
: {
handle: QueryHandle<(input: {
    slug: string;
}) => QueryOptions<readonly ["post", string], Post, Post, 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 postQuery(input: {
    slug: string;
}): QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>
postQuery
> }) {
const [postThenable] =
useQuery<(input: {
    slug: string;
}) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>>(factory: (input: {
    slug: string;
}) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>, handle: QueryHandle<(input: {
    slug: string;
}) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>>, options?: UseQueryOptions): readonly [QueryValue<(input: {
    slug: string;
}) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>>, QueryState<...>]

Read a query by its handle.

useQuery
(
function postQuery(input: {
    slug: string;
}): QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>
postQuery
,
props: {
    handle: QueryHandle<typeof postQuery>;
}
props
.
handle: QueryHandle<(input: {
    slug: string;
}) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>>
handle
);
const postThenable: QueryValue<(input: {
    slug: string;
}) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>>
const const post: Postpost = use<Post>(usable: React.Usable<Post>): Postuse(
const postThenable: QueryValue<(input: {
    slug: string;
}) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>>
postThenable
);
return <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>; }

How a route loads its data

A page says what it needs before it renders. Each query is defined once, the route starts every request when the navigation begins, and components read the results as they arrive.

  1. Define query factories

    A query factory is a function of the query’s input that returns queryOptions, as in TanStack Query: a key, a queryFn, and options like staleTime. Comments load a page at a time, so commentsQuery uses infiniteQueryOptions.

    getPost is a server function: during SSR it runs in the process, and refetches in the browser call it over HTTP.

    Each key starts with a group. comments is a group inside posts, so invalidating posts also matches every post’s comments.

    Every API takes the factory and its input instead of a key: starting a query, reading it, invalidating it. The key and the request are written once, and the data’s type flows from queryFn to the component that reads it.

    The input has to be serializable: it’s what a handle carries from the server to the browser.

    Query factories
    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, }); }
  2. Start them in the route loader

    The router runs the loader as soon as a navigation starts, and with preloading on, when a link is hovered. Every query the page needs starts there, at the same time, before anything renders.

    load takes named entries and starts them together. A prefetch never holds the navigation: the page renders while the post and comments load. load returns a handle for each entry — the query’s key and input, without the data. During SSR, the cache streams to the browser next to those handles, so nothing is fetched twice.

    Use swr when the route can’t lay out without the data. Prefer prefetch when a component will read it under Suspense — the usual shape, and what the example app does for streamed parts of the page.

    Starting queries
    src/routes/blog.$slug.tsx
    export const 
    const Route: Route<Register, RootRoute<Register, undefined, {
        queryClient: QueryClient;
    }, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/blog/$slug", "/blog/$slug", "/blog/$slug", "/blog/$slug", undefined, ResolveParams<"/blog/$slug">, AnyContext, AnyContext, AnyContext, {}, (args: LoaderFnContext<Register, RootRoute<Register, undefined, {
        queryClient: QueryClient;
    }, ... 8 more ..., undefined>, ... 7 more ..., undefined>) => Promise<...>, ... 4 more ..., undefined>
    Route
    =
    createFileRoute<"/blog/$slug", RootRoute<Register, undefined, {
        queryClient: QueryClient;
    }, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/blog/$slug", "/blog/$slug", "/blog/$slug">(path?: "/blog/$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
    ("/blog/$slug")({
    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;
    }, ... 8 more ..., undefined>, ... 7 more ..., undefined>) => Promise<...>, RouteLoaderFn<...> | RouteLoaderObject<...>> | undefined
    loader
    : async (
    args: LoaderFnContext<Register, RootRoute<Register, undefined, {
        queryClient: QueryClient;
    }, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/blog/$slug", ResolveParams<"/blog/$slug">, {}, AnyContext, AnyContext, AnyContext, unknown, undefined>
    args
    ) => {
    const { const slug: stringslug } =
    args: LoaderFnContext<Register, RootRoute<Register, undefined, {
        queryClient: QueryClient;
    }, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/blog/$slug", ResolveParams<"/blog/$slug">, {}, AnyContext, AnyContext, AnyContext, unknown, undefined>
    args
    .
    LoaderFnContext<Register, RootRoute<Register, undefined, { queryClient: QueryClient; }, AnyContext, AnyContext, {}, ... 5 more ..., undefined>, ... 7 more ..., undefined>.params: {
        slug: string;
    }
    params
    ;
    // Never waits: both load while the page renders. const [
    const handles: {
        readonly comments: QueryHandle<(input: {
            slug: string;
        }) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>;
        readonly post: QueryHandle<(input: {
            slug: string;
        }) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>;
    }
    handles
    ] = await
    load<{
        readonly comments: LoadEntry<(input: {
            slug: string;
        }) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>, "prefetch">;
        readonly post: LoadEntry<(input: {
            slug: string;
        }) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>, "prefetch">;
    }>(client: QueryClient, entries: {
        readonly comments: LoadEntry<(input: {
            slug: string;
        }) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>, "prefetch">;
        readonly post: LoadEntry<(input: {
            slug: string;
        }) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, 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, RootRoute<Register, undefined, {
        queryClient: QueryClient;
    }, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/blog/$slug", ResolveParams<"/blog/$slug">, {}, 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<any>queryClient, {
    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 }),
    post: LoadEntry<(input: {
        slug: string;
    }) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>, "prefetch">
    post
    :
    prefetch<(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>, "prefetch">

    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 postQuery(input: {
        slug: string;
    }): QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>
    postQuery
    , { slug: stringslug }),
    }); return
    const handles: {
        readonly comments: QueryHandle<(input: {
            slug: string;
        }) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>;
        readonly post: QueryHandle<(input: {
            slug: string;
        }) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>;
    }
    handles
    ;
    }, UpdatableRouteOptionsExtensions.component?: RouteComponent | undefinedcomponent: function PostPage(): React.JSX.ElementPostPage, });
  3. Read them in components

    useQuery takes the factory and a handle and returns a thenable. use() suspends that subtree while its skeleton shows, so the rest of the page can stream in around it.

    From then on the cache keeps both up to date: it refetches stale data after an action invalidates it and, with recommended(), when the window regains focus, and each component re-renders with the new data.

    Reading queries
    src/routes/blog.$slug.tsx
    function function PostPage(): React.JSX.ElementPostPage() {
    	const 
    const data: {
        readonly comments: QueryHandle<(input: {
            slug: string;
        }) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>;
        readonly post: QueryHandle<(input: {
            slug: string;
        }) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>;
    }
    data
    =
    const Route: Route<Register, RootRoute<Register, undefined, {
        queryClient: QueryClient;
    }, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/blog/$slug", "/blog/$slug", "/blog/$slug", "/blog/$slug", undefined, ResolveParams<"/blog/$slug">, AnyContext, AnyContext, AnyContext, {}, (args: LoaderFnContext<Register, RootRoute<Register, undefined, {
        queryClient: QueryClient;
    }, ... 8 more ..., undefined>, ... 7 more ..., undefined>) => Promise<...>, ... 4 more ..., undefined>
    Route
    .
    RouteExtensions<"/blog/$slug", "/blog/$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<...>>, "/blog/$slug", true, unknown, boolean> & OptionalStructuralSharing<...>) | undefined) => {
        ...;
    }
    useLoaderData
    ();
    return ( <React.JSX.IntrinsicElements.article: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>article> <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 PostSkeleton(): React.ReactNodePostSkeleton />}>
    <
    function Post(props: {
        handle: QueryHandle<typeof postQuery>;
    }): React.JSX.Element
    Post
    handle: QueryHandle<(input: {
        slug: string;
    }) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>
    handle
    ={
    const data: {
        readonly comments: QueryHandle<(input: {
            slug: string;
        }) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>;
        readonly post: QueryHandle<(input: {
            slug: string;
        }) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>;
    }
    data
    .
    post: QueryHandle<(input: {
        slug: string;
    }) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>
    post
    } />
    </const Suspense: React.ExoticComponent<React.SuspenseProps>

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

    Suspense
    >
    <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 PostComments(props: {
        handle: QueryHandle<typeof commentsQuery>;
    }): React.JSX.Element[]
    PostComments
    handle: QueryHandle<(input: {
        slug: string;
    }) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>
    handle
    ={
    const data: {
        readonly comments: QueryHandle<(input: {
            slug: string;
        }) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>;
        readonly post: QueryHandle<(input: {
            slug: string;
        }) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, 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
    >
    </React.JSX.IntrinsicElements.article: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>article> ); } function
    function Post(props: {
        handle: QueryHandle<typeof postQuery>;
    }): React.JSX.Element
    Post
    (
    props: {
        handle: QueryHandle<typeof postQuery>;
    }
    props
    : {
    handle: QueryHandle<(input: {
        slug: string;
    }) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, 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 postQuery(input: {
        slug: string;
    }): QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>
    postQuery
    > }) {
    const [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
    ,
    props: {
        handle: QueryHandle<typeof postQuery>;
    }
    props
    .
    handle: QueryHandle<(input: {
        slug: string;
    }) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>
    handle
    );
    const postThenable: QueryValue<(input: {
        slug: string;
    }) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>
    const const post: Postpost = use<Post>(usable: React.Usable<Post>): Postuse(
    const postThenable: QueryValue<(input: {
        slug: string;
    }) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>
    postThenable
    );
    return <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>; }

A page says what it needs before it renders. Each query is defined once, the server component starts every request when the route renders, and client components read the results as they arrive.

  1. Define query factories

    A query factory is a function of the query’s input that returns queryOptions, as in TanStack Query: a key, a queryFn, and options like staleTime. Comments load a page at a time, so commentsQuery uses infiniteQueryOptions.

    Every API takes the factory and its input instead of a key: starting a query, reading it, invalidating it. The key and the request are written once, and the data’s type flows from queryFn to the component that reads it.

    The input has to be serializable: it’s what a handle carries from the server to the browser.

    Query factories
    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 { function getPost(slug: string): Promise<Post>getPost,
    function listComments(input: {
        cursor: number;
        slug: string;
    }): Promise<CommentPage>
    listComments
    } from "./api.ts";
    const const post: Group<readonly ["post"]>post = group<"post">(name: "post"): Group<readonly ["post"]> (+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
    ("post");
    const const comments: Group<readonly ["comments"]>comments = group<"comments">(name: "comments"): Group<readonly ["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
    ("comments");
    export function
    function postQuery(input: {
        slug: string;
    }): QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>
    postQuery
    (
    input: {
        slug: string;
    }
    input
    : { slug: stringslug: string }) {
    return
    queryOptions<readonly ["post", string], Post, Post>(options: BaseOptions<readonly ["post", string], DefaultTypes> & {
        readonly queryFn: (context: QueryFunctionContext<readonly ["post", string]>) => Awaitable<Post>;
        readonly select?: ((data: Post) => Post) | undefined;
    }): QueryOptions<readonly ["post", string], Post, Post, DefaultTypes> (+1 overload)

    Typed options for a query factory:

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

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

    queryKey
    :
    const post: Group
    <readonly [string]>(rest_0: string) => readonly ["post", string]
    post
    (
    input: {
        slug: string;
    }
    input
    .slug: stringslug),
    queryFn: (context: QueryFunctionContext<readonly ["post", string]>) => Awaitable<Post>queryFn: () => function getPost(slug: string): Promise<Post>getPost(
    input: {
        slug: string;
    }
    input
    .slug: stringslug),
    }); } export function
    function commentsQuery(input: {
        slug: string;
    }): InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>
    commentsQuery
    (
    input: {
        slug: string;
    }
    input
    : { slug: stringslug: string }) {
    return
    infiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>>(options: InfiniteOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes> & {
        readonly queryFn: (context: InfiniteQueryFunctionContext<readonly ["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 ["comments", string], DefaultTypes>.queryKey: readonly ["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 ["comments", string]
    comments
    (
    input: {
        slug: string;
    }
    input
    .slug: stringslug),
    queryFn: (context: InfiniteQueryFunctionContext<readonly ["comments", string], number>) => CommentPage | PromiseLike<CommentPage>queryFn: (context: InfiniteQueryFunctionContext<readonly ["comments", string], number>context) =>
    function listComments(input: {
        cursor: number;
        slug: string;
    }): Promise<CommentPage>
    listComments
    ({ cursor: numbercursor: context: InfiniteQueryFunctionContext<readonly ["comments", string], number>context.InfiniteQueryFunctionContext<readonly ["comments", string], number>.pageParam: numberpageParam, slug: stringslug:
    input: {
        slug: string;
    }
    input
    .slug: stringslug }),
    InfiniteOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>.initialPageParam: numberinitialPageParam: 0, InfiniteOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>.getNextPageParam: PageParamFn<CommentPage, number>getNextPageParam: (lastPage: CommentPagelastPage) => lastPage: CommentPagelastPage.CommentPage.nextCursor: number | nullnextCursor, }); }
  2. Start them in a server component

    A server component runs as soon as the App Router starts rendering the page. Every query the page needs starts there, at the same time, before client components render.

    load takes named entries and starts them together. A prefetch never holds the response: the page streams while the post and comments load. load returns a handle for each entry — the query’s key and input, without the data. HydrationBoundary streams the cache to the browser next to those handles, so nothing is fetched twice.

    Use revalidate when the server component itself renders the data. Prefer prefetch when a client component will read it under Suspense — the usual App Router shape, and what the example app does.

    Starting queries
    src/app/(blog)/posts/[slug]/page.tsx
    export default async function 
    function Page(props: {
        params: Promise<{
            slug: string;
        }>;
    }): Promise<JSX.Element>
    Page
    (
    props: {
        params: Promise<{
            slug: string;
        }>;
    }
    props
    : {
    params: Promise<{
        slug: string;
    }>
    params
    : interface Promise<T>

    Represents the completion of an asynchronous operation

    Promise
    <{ slug: stringslug: string }>;
    }) { const { const slug: stringslug } = await
    props: {
        params: Promise<{
            slug: string;
        }>;
    }
    props
    .
    params: Promise<{
        slug: string;
    }>
    params
    ;
    const
    const qc: QueryClient<{
        readonly meta: Flat<Present<FocusRefetchMeta> & Present<ReconnectRefetchMeta> & Present<PollingMeta> & Present<RetryMeta>>;
    }>
    qc
    =
    function getQueryClient(): QueryClient<{
        readonly meta: Flat<Present<FocusRefetchMeta> & Present<ReconnectRefetchMeta> & Present<PollingMeta> & Present<RetryMeta>>;
    }>

    The server components' client, one per request.

    getQueryClient
    ();
    // Never waits: both load while the page streams. const [
    const handles: {
        readonly comments: QueryHandle<(input: {
            slug: string;
        }) => InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>;
        readonly post: QueryHandle<(input: {
            slug: string;
        }) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>>;
    }
    handles
    ] = await
    load<{
        readonly comments: LoadEntry<(input: {
            slug: string;
        }) => InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>, "prefetch">;
        readonly post: LoadEntry<(input: {
            slug: string;
        }) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>, "prefetch">;
    }>(client: QueryClient, entries: {
        readonly comments: LoadEntry<(input: {
            slug: string;
        }) => InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>, "prefetch">;
        readonly post: LoadEntry<(input: {
            slug: string;
        }) => QueryOptions<readonly ["post", string], Post, Post, 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
    (
    const qc: QueryClient<{
        readonly meta: Flat<Present<FocusRefetchMeta> & Present<ReconnectRefetchMeta> & Present<PollingMeta> & Present<RetryMeta>>;
    }>
    qc
    , {
    comments: LoadEntry<(input: {
        slug: string;
    }) => InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>, "prefetch">
    comments
    :
    prefetch<(input: {
        slug: string;
    }) => InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>(factory: (input: {
        slug: string;
    }) => InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>, ...args: LoadEntryArgs<(input: {
        slug: string;
    }) => InfiniteQueryOptions<readonly ["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 ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>
    commentsQuery
    , { slug: stringslug }),
    post: LoadEntry<(input: {
        slug: string;
    }) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>, "prefetch">
    post
    :
    prefetch<(input: {
        slug: string;
    }) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>>(factory: (input: {
        slug: string;
    }) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>, ...args: LoadEntryArgs<(input: {
        slug: string;
    }) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>>): LoadEntry<(input: {
        slug: string;
    }) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>, "prefetch">

    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 postQuery(input: {
        slug: string;
    }): QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>
    postQuery
    , { slug: stringslug }),
    }); return ( <function HydrationBoundary(props: HydrationBoundaryProps): JSX.Element

    Writes server data into the browser (or SSR) client.

    HydrationBoundary
    client: QueryClient<any>

    The request's server client (e.g. from React's cache()). Server components only.

    client
    ={
    const qc: QueryClient<{
        readonly meta: Flat<Present<FocusRefetchMeta> & Present<ReconnectRefetchMeta> & Present<PollingMeta> & Present<RetryMeta>>;
    }>
    qc
    }>
    <JSX.IntrinsicElements.article: DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>article> <const Suspense: ExoticComponent<SuspenseProps>

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

    Suspense
    SuspenseProps.fallback?: ReactNode

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

    fallback
    ={<function PostSkeleton(): JSX.ElementPostSkeleton />}>
    <
    function Post(props: {
        handle: QueryHandle<typeof postQuery>;
    }): JSX.Element
    Post
    handle: QueryHandle<(input: {
        slug: string;
    }) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>>
    handle
    ={
    const handles: {
        readonly comments: QueryHandle<(input: {
            slug: string;
        }) => InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>;
        readonly post: QueryHandle<(input: {
            slug: string;
        }) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>>;
    }
    handles
    .
    post: QueryHandle<(input: {
        slug: string;
    }) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>>
    post
    } />
    </const Suspense: ExoticComponent<SuspenseProps>

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

    Suspense
    >
    <const Suspense: ExoticComponent<SuspenseProps>

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

    Suspense
    SuspenseProps.fallback?: ReactNode

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

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

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

    Suspense
    >
    </JSX.IntrinsicElements.article: DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>article> </function HydrationBoundary(props: HydrationBoundaryProps): JSX.Element

    Writes server data into the browser (or SSR) client.

    HydrationBoundary
    >
    ); }
  3. Read them in client components

    Client components live in their own files, marked with a use client directive. useQuery takes the factory and a handle and returns a thenable. use() suspends that subtree while its skeleton shows, so the rest of the page can stream in around it.

    From then on the cache keeps both up to date: it refetches stale data after an action invalidates it and, with recommended(), when the window regains focus, and each component re-renders with the new data.

    Reading queries
    src/app/(blog)/components/post.tsx
    "use client";
    export function 
    function Post(props: {
        handle: QueryHandle<typeof postQuery>;
    }): JSX.Element
    Post
    (
    props: {
        handle: QueryHandle<typeof postQuery>;
    }
    props
    : {
    handle: QueryHandle<(input: {
        slug: string;
    }) => QueryOptions<readonly ["post", string], Post, Post, 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 postQuery(input: {
        slug: string;
    }): QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>
    postQuery
    > }) {
    const [postThenable] =
    useQuery<(input: {
        slug: string;
    }) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>>(factory: (input: {
        slug: string;
    }) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>, handle: QueryHandle<(input: {
        slug: string;
    }) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>>, options?: UseQueryOptions): readonly [QueryValue<(input: {
        slug: string;
    }) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>>, QueryState<...>]

    Read a query by its handle.

    useQuery
    (
    function postQuery(input: {
        slug: string;
    }): QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>
    postQuery
    ,
    props: {
        handle: QueryHandle<typeof postQuery>;
    }
    props
    .
    handle: QueryHandle<(input: {
        slug: string;
    }) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>>
    handle
    );
    const postThenable: QueryValue<(input: {
        slug: string;
    }) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>>
    const const post: Postpost = use<Post>(usable: Usable<Post>): Postuse(
    const postThenable: QueryValue<(input: {
        slug: string;
    }) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>>
    postThenable
    );
    return <JSX.IntrinsicElements.h1: DetailedHTMLProps<HTMLAttributes<HTMLHeadingElement>, HTMLHeadingElement>h1>{const post: Postpost.Post.title: stringtitle}</JSX.IntrinsicElements.h1: DetailedHTMLProps<HTMLAttributes<HTMLHeadingElement>, HTMLHeadingElement>h1>; }
    src/app/(blog)/components/comments.tsx
    "use client";
    export function 
    function Comments(props: {
        handle: QueryHandle<typeof commentsQuery>;
    }): JSX.Element[]
    Comments
    (
    props: {
        handle: QueryHandle<typeof commentsQuery>;
    }
    props
    : {
    handle: QueryHandle<(input: {
        slug: string;
    }) => InfiniteQueryOptions<readonly ["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 ["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 ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>
    pagesThenable
    ] =
    useInfiniteQuery<(input: {
        slug: string;
    }) => InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>(factory: (input: {
        slug: string;
    }) => InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>, handle: QueryHandle<(input: {
        slug: string;
    }) => InfiniteQueryOptions<readonly ["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 ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>
    commentsQuery
    ,
    props: {
        handle: QueryHandle<typeof commentsQuery>;
    }
    props
    .
    handle: QueryHandle<(input: {
        slug: string;
    }) => InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>
    handle
    );
    const pages = use<InfiniteData<CommentPage, number>>(usable: Usable<InfiniteData<CommentPage, number>>): InfiniteData<CommentPage, number>use(
    const pagesThenable: QueryValue<(input: {
        slug: string;
    }) => InfiniteQueryOptions<readonly ["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<JSX.Element>(callbackfn: (value: PostComment, index: number, array: PostComment[]) => JSX.Element, thisArg?: any): 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
    ((comment: PostCommentcomment) => <
    function Comment(props: {
        comment: PostComment;
    }): React.ReactNode
    Comment
    Attributes.key?: Key | null | undefinedkey={comment: PostCommentcomment.PostComment.id: stringid} comment: PostCommentcomment={comment: PostCommentcomment} />);
    }

A page says what it needs before it renders. Each query is defined once, the route’s loader starts every request when the navigation begins, and components read the results as they arrive.

  1. Define query factories

    A query factory is a function of the query’s input that returns queryOptions, as in TanStack Query: a key, a queryFn, and options like staleTime. Comments load a page at a time, so commentsQuery uses infiniteQueryOptions.

    Every API takes the factory and its input instead of a key: starting a query, reading it, invalidating it. The key and the request are written once, and the data’s type flows from queryFn to the component that reads it.

    The input has to be serializable: it’s what a handle carries from the server to the browser.

    Query factories
    app/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 { function getPost(slug: string): Promise<Post>getPost,
    function listComments(input: {
        cursor: number;
        slug: string;
    }): Promise<CommentPage>
    listComments
    } from "./api.ts";
    const const post: Group<readonly ["post"]>post = group<"post">(name: "post"): Group<readonly ["post"]> (+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
    ("post");
    const const comments: Group<readonly ["comments"]>comments = group<"comments">(name: "comments"): Group<readonly ["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
    ("comments");
    export function
    function postQuery(input: {
        slug: string;
    }): QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>
    postQuery
    (
    input: {
        slug: string;
    }
    input
    : { slug: stringslug: string }) {
    return
    queryOptions<readonly ["post", string], Post, Post>(options: BaseOptions<readonly ["post", string], DefaultTypes> & {
        readonly queryFn: (context: QueryFunctionContext<readonly ["post", string]>) => Awaitable<Post>;
        readonly select?: ((data: Post) => Post) | undefined;
    }): QueryOptions<readonly ["post", string], Post, Post, DefaultTypes> (+1 overload)

    Typed options for a query factory:

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

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

    queryKey
    :
    const post: Group
    <readonly [string]>(rest_0: string) => readonly ["post", string]
    post
    (
    input: {
        slug: string;
    }
    input
    .slug: stringslug),
    queryFn: (context: QueryFunctionContext<readonly ["post", string]>) => Awaitable<Post>queryFn: () => function getPost(slug: string): Promise<Post>getPost(
    input: {
        slug: string;
    }
    input
    .slug: stringslug),
    }); } export function
    function commentsQuery(input: {
        slug: string;
    }): InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>
    commentsQuery
    (
    input: {
        slug: string;
    }
    input
    : { slug: stringslug: string }) {
    return
    infiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>>(options: InfiniteOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes> & {
        readonly queryFn: (context: InfiniteQueryFunctionContext<readonly ["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 ["comments", string], DefaultTypes>.queryKey: readonly ["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 ["comments", string]
    comments
    (
    input: {
        slug: string;
    }
    input
    .slug: stringslug),
    queryFn: (context: InfiniteQueryFunctionContext<readonly ["comments", string], number>) => CommentPage | PromiseLike<CommentPage>queryFn: (context: InfiniteQueryFunctionContext<readonly ["comments", string], number>context) =>
    function listComments(input: {
        cursor: number;
        slug: string;
    }): Promise<CommentPage>
    listComments
    ({ cursor: numbercursor: context: InfiniteQueryFunctionContext<readonly ["comments", string], number>context.InfiniteQueryFunctionContext<readonly ["comments", string], number>.pageParam: numberpageParam, slug: stringslug:
    input: {
        slug: string;
    }
    input
    .slug: stringslug }),
    InfiniteOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>.initialPageParam: numberinitialPageParam: 0, InfiniteOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>.getNextPageParam: PageParamFn<CommentPage, number>getNextPageParam: (lastPage: CommentPagelastPage) => lastPage: CommentPagelastPage.CommentPage.nextCursor: number | nullnextCursor, }); }
  2. Start them in the route loader

    React Router runs the loader on the server as soon as a navigation starts. Every query the page needs starts there, at the same time, before anything renders. A root middleware creates one client per request, and every loader reads it from the context: nested routes share a cache, and requests never share data.

    load takes named entries and starts them together. A prefetch never holds the navigation: the page renders while the post and comments load. load returns a handle for each entry — the query’s key and input, without the data. The loader returns the handles with dehydrate(qc), and React Router streams the queries still loading to the browser, so nothing is fetched twice.

    Use swr when the route can’t lay out without the data. Prefer prefetch when a component will read it under Suspense — the usual shape, and what the example app does for streamed parts of the page.

    React Router
    app/routes/blog.$slug.tsx
    export async function 
    function loader(args: Route.LoaderArgs): Promise<{
        dehydratedState: DehydratedState;
        comments: QueryHandle<(input: {
            slug: string;
        }) => InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>;
        post: QueryHandle<(input: {
            slug: string;
        }) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>>;
    }>
    loader
    (
    args: CreateServerLoaderArgs<Info & {
        module: typeof import("./blog.$slug");
        matches: Matches;
    }>
    args
    : Route.
    type Route.LoaderArgs = {
        request: Request;
        url: URL;
        params: {
            slug: string;
        };
        pattern: string;
        context: Readonly<RouterContextProvider>;
    }
    LoaderArgs
    ) {
    const const qc: QueryClient<any>qc =
    args: CreateServerLoaderArgs<Info & {
        module: typeof import("./blog.$slug");
        matches: Matches;
    }>
    args
    .context: Readonly<RouterContextProvider>

    An instance of RouterContextProvider that can be used for type-safe access to context values set in your route middlewares. If you are using a custom server adapter, you may provide an initial set of context values from your getLoadContext function.

    context
    .get<QueryClient<any>>(context: RouterContext<QueryClient<any>>): QueryClient<any>

    Access a value from the context. If no value has been set for the context, it will return the context's defaultValue if provided, or throw an error if no defaultValue was set.

    @paramcontext — The context to get the value for@returnsThe value for the context, or the context's defaultValue if no value was set
    get
    (const queryClientContext: RouterContext<QueryClient<any>>queryClientContext);
    const { const slug: stringslug } =
    args: CreateServerLoaderArgs<Info & {
        module: typeof import("./blog.$slug");
        matches: Matches;
    }>
    args
    .
    params: {
        slug: string;
    }

    Dynamic route params for the current route.

    params
    ;
    // Never waits: both load while the page renders. const [
    const handles: {
        readonly comments: QueryHandle<(input: {
            slug: string;
        }) => InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>;
        readonly post: QueryHandle<(input: {
            slug: string;
        }) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>>;
    }
    handles
    ] = await
    load<{
        readonly comments: LoadEntry<(input: {
            slug: string;
        }) => InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>, "prefetch">;
        readonly post: LoadEntry<(input: {
            slug: string;
        }) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>, "prefetch">;
    }>(client: QueryClient, entries: {
        readonly comments: LoadEntry<(input: {
            slug: string;
        }) => InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>, "prefetch">;
        readonly post: LoadEntry<(input: {
            slug: string;
        }) => QueryOptions<readonly ["post", string], Post, Post, 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
    (const qc: QueryClient<any>qc, {
    comments: LoadEntry<(input: {
        slug: string;
    }) => InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>, "prefetch">
    comments
    :
    prefetch<(input: {
        slug: string;
    }) => InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>(factory: (input: {
        slug: string;
    }) => InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>, ...args: LoadEntryArgs<(input: {
        slug: string;
    }) => InfiniteQueryOptions<readonly ["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 ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>
    commentsQuery
    , { slug: stringslug }),
    post: LoadEntry<(input: {
        slug: string;
    }) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>, "prefetch">
    post
    :
    prefetch<(input: {
        slug: string;
    }) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>>(factory: (input: {
        slug: string;
    }) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>, ...args: LoadEntryArgs<(input: {
        slug: string;
    }) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>>): LoadEntry<(input: {
        slug: string;
    }) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>, "prefetch">

    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 postQuery(input: {
        slug: string;
    }): QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>
    postQuery
    , { slug: stringslug }),
    }); return { ...
    const handles: {
        readonly comments: QueryHandle<(input: {
            slug: string;
        }) => InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>;
        readonly post: QueryHandle<(input: {
            slug: string;
        }) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>>;
    }
    handles
    , dehydratedState: DehydratedStatedehydratedState: function dehydrate(client: QueryClient, options?: DehydrateOptions): DehydratedState

    Snapshot the client's entries for transfer.

    dehydrate
    (const qc: QueryClient<any>qc) };
    }
  3. Read them in components

    The route component wraps the page in HydrationBoundary, which writes the loader’s snapshot into the browser’s cache. useQuery takes the factory and a handle and returns a thenable. use() suspends that subtree while its skeleton shows, so the rest of the page can stream in around it.

    From then on the cache keeps both up to date: it refetches stale data after an action invalidates it and, with recommended(), when the window regains focus, and each component re-renders with the new data.

    Reading queries
    app/routes/blog.$slug.tsx
    export default function function PostPage(props: Route.ComponentProps): React.JSX.ElementPostPage(
    props: CreateComponentProps<Info & {
        module: typeof import("./blog.$slug");
        matches: Matches;
    }, false>
    props
    : Route.
    type Route.ComponentProps = {
        params: {
            slug: string;
        };
        matches: [{
            id: "root";
            params: Record<string, string | undefined>;
            pathname: string;
            loaderData: undefined;
            handle: unknown;
        }, {
            id: "routes/blog.$slug";
            params: Record<string, string | undefined>;
            pathname: string;
            loaderData: {
                dehydratedState: {
                    readonly entries: readonly {
                        readonly data?: undefined;
                        readonly promise?: Promise<{
                            readonly data?: undefined;
                            readonly updatedAt: number;
                        }> | undefined;
                        readonly queryKey: readonly undefined[];
                        readonly updatedAt: number;
                    }[];
                };
                comments: {
                    readonly input: {
                        slug: string;
                    };
                    readonly queryKey: ["comments", string];
                };
                post: {
                    ...;
                };
            };
            handle: unknown;
        }, ...({
            ...;
        } | undefined)[]];
    } & {
        ...;
    }
    ComponentProps
    ) {
    const {
    const comments: {
        readonly input: {
            slug: string;
        };
        readonly queryKey: ["comments", string];
    }
    comments
    ,
    const dehydratedState: {
        readonly entries: readonly {
            readonly data?: undefined;
            readonly promise?: Promise<{
                readonly data?: undefined;
                readonly updatedAt: number;
            }> | undefined;
            readonly queryKey: readonly undefined[];
            readonly updatedAt: number;
        }[];
    }
    dehydratedState
    ,
    const post: {
        readonly input: {
            slug: string;
        };
        readonly queryKey: ["post", string];
    }
    post
    } =
    props: CreateComponentProps<Info & {
        module: typeof import("./blog.$slug");
        matches: Matches;
    }, false>
    props
    .
    loaderData: {
        dehydratedState: {
            readonly entries: readonly {
                readonly data?: undefined;
                readonly promise?: Promise<{
                    readonly data?: undefined;
                    readonly updatedAt: number;
                }> | undefined;
                readonly queryKey: readonly undefined[];
                readonly updatedAt: number;
            }[];
        };
        comments: {
            readonly input: {
                slug: string;
            };
            readonly queryKey: ["comments", string];
        };
        post: {
            ...;
        };
    }

    The data returned from the loader or clientLoader

    loaderData
    ;
    return ( <function HydrationBoundary(props: HydrationBoundaryProps): React.JSX.Element

    Writes server data into the browser (or SSR) client.

    HydrationBoundary
    state: DehydratedState

    A loader's dehydrate(client).

    state
    ={
    const dehydratedState: {
        readonly entries: readonly {
            readonly data?: undefined;
            readonly promise?: Promise<{
                readonly data?: undefined;
                readonly updatedAt: number;
            }> | undefined;
            readonly queryKey: readonly undefined[];
            readonly updatedAt: number;
        }[];
    }
    dehydratedState
    }>
    <React.JSX.IntrinsicElements.article: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>article> <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 PostSkeleton(): React.ReactNodePostSkeleton />}>
    <
    function Post(props: {
        handle: QueryHandle<typeof postQuery>;
    }): React.JSX.Element
    Post
    handle: QueryHandle<(input: {
        slug: string;
    }) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>>
    handle
    ={
    const post: {
        readonly input: {
            slug: string;
        };
        readonly queryKey: ["post", string];
    }
    post
    } />
    </const Suspense: React.ExoticComponent<React.SuspenseProps>

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

    Suspense
    >
    <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 PostComments(props: {
        handle: QueryHandle<typeof commentsQuery>;
    }): React.JSX.Element[]
    PostComments
    handle: QueryHandle<(input: {
        slug: string;
    }) => InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>
    handle
    ={
    const comments: {
        readonly input: {
            slug: string;
        };
        readonly queryKey: ["comments", string];
    }
    comments
    } />
    </const Suspense: React.ExoticComponent<React.SuspenseProps>

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

    Suspense
    >
    </React.JSX.IntrinsicElements.article: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>article> </function HydrationBoundary(props: HydrationBoundaryProps): React.JSX.Element

    Writes server data into the browser (or SSR) client.

    HydrationBoundary
    >
    ); } function
    function Post(props: {
        handle: QueryHandle<typeof postQuery>;
    }): React.JSX.Element
    Post
    (
    props: {
        handle: QueryHandle<typeof postQuery>;
    }
    props
    : {
    handle: QueryHandle<(input: {
        slug: string;
    }) => QueryOptions<readonly ["post", string], Post, Post, 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 postQuery(input: {
        slug: string;
    }): QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>
    postQuery
    > }) {
    const [postThenable] =
    useQuery<(input: {
        slug: string;
    }) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>>(factory: (input: {
        slug: string;
    }) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>, handle: QueryHandle<(input: {
        slug: string;
    }) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>>, options?: UseQueryOptions): readonly [QueryValue<(input: {
        slug: string;
    }) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>>, QueryState<...>]

    Read a query by its handle.

    useQuery
    (
    function postQuery(input: {
        slug: string;
    }): QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>
    postQuery
    ,
    props: {
        handle: QueryHandle<typeof postQuery>;
    }
    props
    .
    handle: QueryHandle<(input: {
        slug: string;
    }) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>>
    handle
    );
    const postThenable: QueryValue<(input: {
        slug: string;
    }) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>>
    const const post: Postpost = use<Post>(usable: React.Usable<Post>): Postuse(
    const postThenable: QueryValue<(input: {
        slug: string;
    }) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>>
    postThenable
    );
    return <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>; }

Built around how React wants data

Fetch-on-render is an explicitly named escape hatch, not the default. Every page starts its queries before it renders.

Try it: prefetch, swr or revalidate

The demo runs in your browser against a fake server, with its queries, their requests and the cache shown next to it.

Standing on TanStack Query’s shoulders

The cache’s mechanics follow TanStack Query closely: query keys, deduping, staleness, garbage collection, structural sharing and infinite pages. Retries and focus and reconnect refetching are there too, as links and features you turn on. If you know TanStack Query, you already know how raf-query caches. What changes is where queries start and how components read them.

TanStack Queryraf-query
queryClient.prefetchQuery()load(qc, { x: prefetch(factory, input) })
queryClient.ensureQueryData()load(qc, { x: swr(factory, input) })
queryClient.fetchQuery()load(qc, { x: revalidate(factory, input) })
useSuspenseQuery()use() on the thenable from useQuery(factory, handle)
useInfiniteQuery()useQuery() with an infiniteQueryOptions factory
placeholderData: keepPreviousDatawhilePending: "previous"
useMutation()a React action: await write(), then invalidateMany()
invalidateQueries(filters)invalidateMany(qc, target)

What we leave out

TanStack Query is mature and broad. raf-query is narrower: the same cache ideas, shaped so queries start in loaders and RSC. These are deliberate gaps, not unfinished ports.

  • useMutationActions, useOptimistic and useActionState already cover pending, optimistic and error state.
  • enabled / placeholderDataThe route owns the input, so it starts a query only once the input exists, or skips it with skipToken. whilePending keeps the previous page.
  • refetchOnMountMounting a reader never fetches. Loaders re-run on navigation.
  • Render-starting useQueryOnly a waterfallHandle may start a request in render — waterfalls stay explicit.
  • Persistence and other frameworksA React + Suspense experiment, not a multi-framework ecosystem.

Everything we leave out, and why