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
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.
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.ReactNodeA 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.ReactNodeA 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 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>;
}
function function PostComments(props: {
handle: QueryHandle<typeof commentsQuery>;
}): React.JSX.Element[]
PostComments(props: {
handle: QueryHandle<typeof commentsQuery>;
}
props: { handle: QueryHandle<(input: {
slug: string;
}) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>
handle: interface QueryHandle<TFactory extends QueryFactory = (input: unknown) => { readonly queryFn: (context: never) => unknown; readonly queryKey: QueryKey; }>A small serializable reference to a query: its key and its factory's
input. It carries no data; readers get that from the cache. Handles of
different factories have different types.
QueryHandle<typeof function commentsQuery(input: {
slug: string;
}): InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>
commentsQuery> }) {
// May still be loading. If so, use() suspends until it lands.
const [const pagesThenable: QueryValue<(input: {
slug: string;
}) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>
pagesThenable] = useInfiniteQuery<(input: {
slug: string;
}) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>(factory: (input: {
slug: string;
}) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>, handle: QueryHandle<(input: {
slug: string;
}) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>, options?: UseQueryOptions): readonly [...]
Read a paged query (infiniteQueryOptions) by its handle. The data is
{ pages, pageParams }, and its state loads more pages.
useInfiniteQuery(function commentsQuery(input: {
slug: string;
}): InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>
commentsQuery, props: {
handle: QueryHandle<typeof commentsQuery>;
}
props.handle: QueryHandle<(input: {
slug: string;
}) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>
handle);
const const pages: InfiniteData<CommentPage, number>pages = use<InfiniteData<CommentPage, number>>(usable: React.Usable<InfiniteData<CommentPage, number>>): InfiniteData<CommentPage, number>use(const pagesThenable: QueryValue<(input: {
slug: string;
}) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>
pagesThenable);
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.
flatMap((page: CommentPagepage) => page: CommentPagepage.CommentPage.items: readonly PostComment[]items).Array<PostComment>.map<React.JSX.Element>(callbackfn: (value: PostComment, index: number, array: PostComment[]) => React.JSX.Element, thisArg?: any): React.JSX.Element[]Calls a defined callback function on each element of an array, and returns an array that contains the results.
map(function renderComment(comment: PostComment): React.JSX.ElementrenderComment);
}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.ElementWrites 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?: ReactNodeA 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?: ReactNodeA 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.ElementWrites server data into the browser (or SSR) client.
HydrationBoundary>
);
}"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 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>;
}"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); 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.
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.
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} />);
}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.
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): DehydratedStateSnapshot 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.ElementWrites server data into the browser (or SSR) client.
HydrationBoundary state: DehydratedStateA 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.ReactNodeA 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.ReactNodeA 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.ElementWrites 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 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>;
}
function function PostComments(props: {
handle: QueryHandle<typeof commentsQuery>;
}): React.JSX.Element[]
PostComments(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 const pages: InfiniteData<CommentPage, number>pages = use<InfiniteData<CommentPage, number>>(usable: React.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);
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.
flatMap((page: CommentPagepage) => page: CommentPagepage.CommentPage.items: readonly PostComment[]items)
.Array<PostComment>.map<React.JSX.Element>(callbackfn: (value: PostComment, index: number, array: PostComment[]) => React.JSX.Element, thisArg?: any): React.JSX.Element[]Calls a defined callback function on each element of an array, and returns an array that contains the results.
map((comment: PostCommentcomment) => <function Comment(props: {
comment: PostComment;
}): React.ReactNode
Comment React.Attributes.key?: React.Key | null | undefinedkey={comment: PostCommentcomment.PostComment.id: stringid} comment: PostCommentcomment={comment: PostCommentcomment} />);
}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.
Define query factories
Query factoriesA query factory is a function of the query’s input that returns
queryOptions, as in TanStack Query: a key, aqueryFn, and options likestaleTime. Comments load a page at a time, socommentsQueryusesinfiniteQueryOptions.getPostis 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.
commentsis a group insideposts, so invalidatingpostsalso 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
queryFnto the component that reads it.The input has to be serializable: it’s what a handle carries from the server to the browser.
src/queries.tsimport {function group<const TName extends string>(name: TName): Group<readonly [TName]> (+1 overload)group,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.const infiniteQueryOptions: InfiniteQueryOptionsBuilder<DefaultTypes>infiniteQueryOptions,Typed options for a paged query factory. Readers get
{ pages, pageParams }and, on their state,fetchNextPageandfetchPreviousPage:const queryOptions: QueryOptionsBuilder<DefaultTypes>queryOptions } from "@raf-query/react"; import { type CommentPage,Typed options for a query factory:
listComments } from "./api.ts"; import {function listComments(input: { cursor: number; slug: string; }): Promise<CommentPage>getPost } from "./posts.functions.ts"; export constconst getPost: RequiredFetcher<undefined, ZodObject<{ slug: ZodString; }, $strip>, Promise<Post>>const posts: Group<readonly ["posts"]>posts =group<"posts">(name: "posts"): Group<readonly ["posts"]> (+1 overload)group("posts"); // A post's comments are a group inside `posts`. export constA 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.const comments: Group<readonly ["posts", "comments"]>comments =group<readonly ["posts"], "comments">(parent: Group<readonly ["posts"]>, name: "comments"): Group<readonly ["posts", "comments"]> (+1 overload)group(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.const posts: Group<readonly ["posts"]>posts, "comments"); export functionpostQuery(function postQuery(input: { slug: string; }): QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>input: {input: { slug: string; }slug: stringslug: string }) { returnqueryOptions<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)queryOptions({Typed options for a query factory:
BaseOptions<readonly ["posts", "bySlug", string], DefaultTypes>.queryKey: readonly ["posts", "bySlug", string]queryKey:A plain array, usually built by a
group(). Handles keep its literal type.posts("bySlug",const posts: Group <readonly ["bySlug", string]>(rest_0: "bySlug", rest_1: string) => readonly ["posts", "bySlug", string]input.input: { slug: string; }slug: stringslug),queryFn: (context: QueryFunctionContext<readonly ["posts", "bySlug", string]>) => Awaitable<Post>queryFn: () =>getPost({function getPost(opts: RequiredFetcherDataOptions<undefined, ZodObject<{ slug: ZodString; }, $strip>>): Promise<Post>data:RequiredFetcherDataOptions<undefined, ZodObject<{ slug: ZodString; }, $strip>>.data: { slug: string; }input }), }); } export functioninput: { slug: string; }commentsQuery(function commentsQuery(input: { slug: string; }): InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>input: {input: { slug: string; }slug: stringslug: string }) { returninfiniteQueryOptions<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<...>infiniteQueryOptions({Typed options for a paged query factory. Readers get
{ pages, pageParams }and, on their state,fetchNextPageandfetchPreviousPage:BaseOptions<readonly ["posts", "comments", string], DefaultTypes>.queryKey: readonly ["posts", "comments", string]queryKey:A plain array, usually built by a
group(). Handles keep its literal type.comments(const comments: Group <readonly [string]>(rest_0: string) => readonly ["posts", "comments", string]input.input: { slug: string; }slug: stringslug),queryFn: (context: InfiniteQueryFunctionContext<readonly ["posts", "comments", string], number>) => CommentPage | PromiseLike<CommentPage>queryFn: (context: InfiniteQueryFunctionContext<readonly ["posts", "comments", string], number>context) =>listComments({function listComments(input: { cursor: number; slug: string; }): Promise<CommentPage>cursor: numbercursor:context: InfiniteQueryFunctionContext<readonly ["posts", "comments", string], number>context.InfiniteQueryFunctionContext<readonly ["posts", "comments", string], number>.pageParam: numberpageParam,slug: stringslug:input.input: { slug: string; }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, }); }Start them in the route loader
Starting queriesThe 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.
loadtakes named entries and starts them together. Aprefetchnever holds the navigation: the page renders while the post and comments load.loadreturns 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
swrwhen the route can’t lay out without the data. Preferprefetchwhen a component will read it under Suspense — the usual shape, and what the example app does for streamed parts of the page.src/routes/blog.$slug.tsxexport constRoute =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>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<...>createFileRoute("/blog/$slug")({Creates a file-based Route factory for a given path.
loader: async (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<...>> | undefinedargs) => { const {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>const slug: stringslug } =args.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>params; // Never waits: both load while the page renders. const [LoaderFnContext<Register, RootRoute<Register, undefined, { queryClient: QueryClient; }, AnyContext, AnyContext, {}, ... 5 more ..., undefined>, ... 7 more ..., undefined>.params: { slug: string; }handles] = awaitconst 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>>; }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<...>load(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 everyswrandrevalidateentry has data, and rejects with the error as soon as one fails.prefetchentries never make it reject. AskipTokenin place of an entry starts nothing and gets no handle.args.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>context.LoaderFnContext<Register, RootRoute<Register, undefined, { queryClient: QueryClient; }, AnyContext, AnyContext, ... 6 more ..., undefined>, ... 7 more ..., undefined>.context: { queryClient: QueryClient; }queryClient: QueryClient<any>queryClient, {comments:comments: LoadEntry<(input: { slug: string; }) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>, "prefetch">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<...>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.
commentsQuery, {function commentsQuery(input: { slug: string; }): InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>slug: stringslug }),post:post: LoadEntry<(input: { slug: string; }) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>, "prefetch">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">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.
postQuery, {function postQuery(input: { slug: string; }): QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>slug: stringslug }), }); returnhandles; },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>>; }UpdatableRouteOptionsExtensions.component?: RouteComponent | undefinedcomponent:function PostPage(): React.JSX.ElementPostPage, });Read them in components
Reading queriesuseQuerytakes 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.src/routes/blog.$slug.tsxfunctionfunction PostPage(): React.JSX.ElementPostPage() { constdata =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>>; }Route.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>useLoaderData(); return ( <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) => { ...; }React.JSX.IntrinsicElements.article: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>article> <const Suspense: React.ExoticComponent<React.SuspenseProps>SuspenseLets you display a fallback until its children have finished loading.
React.SuspenseProps.fallback?: React.ReactNodefallback={<A fallback react tree to show when a Suspense child (like React.lazy) suspends
function PostSkeleton(): React.ReactNodePostSkeleton />}> <Postfunction Post(props: { handle: QueryHandle<typeof postQuery>; }): React.JSX.Elementhandle={handle: QueryHandle<(input: { slug: string; }) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>data.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>>; }post} /> </post: QueryHandle<(input: { slug: string; }) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>const Suspense: React.ExoticComponent<React.SuspenseProps>Suspense> <Lets you display a fallback until its children have finished loading.
const Suspense: React.ExoticComponent<React.SuspenseProps>SuspenseLets you display a fallback until its children have finished loading.
React.SuspenseProps.fallback?: React.ReactNodefallback={<A fallback react tree to show when a Suspense child (like React.lazy) suspends
function CommentsSkeleton(): React.JSX.ElementCommentsSkeleton />}> <PostCommentsfunction PostComments(props: { handle: QueryHandle<typeof commentsQuery>; }): React.JSX.Element[]handle={handle: QueryHandle<(input: { slug: string; }) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>data.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>>; }comments} /> </comments: QueryHandle<(input: { slug: string; }) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>const Suspense: React.ExoticComponent<React.SuspenseProps>Suspense> </Lets you display a fallback until its children have finished loading.
React.JSX.IntrinsicElements.article: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>article> ); } functionPost(function Post(props: { handle: QueryHandle<typeof postQuery>; }): React.JSX.Elementprops: {props: { handle: QueryHandle<typeof postQuery>; }handle:handle: QueryHandle<(input: { slug: string; }) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>interface QueryHandle<TFactory extends QueryFactory = (input: unknown) => { readonly queryFn: (context: never) => unknown; readonly queryKey: QueryKey; }>QueryHandle<typeofA 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.
postQuery> }) { const [postThenable] =function postQuery(input: { slug: string; }): QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>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 [...]useQuery(Read a query by its handle.
postQuery,function postQuery(input: { slug: string; }): QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>props.props: { handle: QueryHandle<typeof postQuery>; }handle); consthandle: QueryHandle<(input: { slug: string; }) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>const post: Postpost =use<Post>(usable: React.Usable<Post>): Postuse(postThenable); return <const postThenable: QueryValue<(input: { slug: string; }) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>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>; } functionPostComments(function PostComments(props: { handle: QueryHandle<typeof commentsQuery>; }): React.JSX.Element[]props: {props: { handle: QueryHandle<typeof commentsQuery>; }handle:handle: QueryHandle<(input: { slug: string; }) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>interface QueryHandle<TFactory extends QueryFactory = (input: unknown) => { readonly queryFn: (context: never) => unknown; readonly queryKey: QueryKey; }>QueryHandle<typeofA 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.
commentsQuery> }) { // May still be loading. If so, use() suspends until it lands. const [function commentsQuery(input: { slug: string; }): InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>pagesThenable] =const pagesThenable: QueryValue<(input: { slug: string; }) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>useInfiniteQuery<(input: { slug: string; }) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>(factory: (input: { slug: string; }) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>, handle: QueryHandle<(input: { slug: string; }) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>, options?: UseQueryOptions): readonly [...]useInfiniteQuery(Read a paged query (
infiniteQueryOptions) by its handle. The data is{ pages, pageParams }, and its state loads more pages.commentsQuery,function commentsQuery(input: { slug: string; }): InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>props.props: { handle: QueryHandle<typeof commentsQuery>; }handle); consthandle: QueryHandle<(input: { slug: string; }) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>const pages: InfiniteData<CommentPage, number>pages =use<InfiniteData<CommentPage, number>>(usable: React.Usable<InfiniteData<CommentPage, number>>): InfiniteData<CommentPage, number>use(pagesThenable); returnconst pagesThenable: QueryValue<(input: { slug: string; }) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>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[]flatMap((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.
page: CommentPagepage) =>page: CommentPagepage.CommentPage.items: readonly PostComment[]items).Array<PostComment>.map<React.JSX.Element>(callbackfn: (value: PostComment, index: number, array: PostComment[]) => React.JSX.Element, thisArg?: any): React.JSX.Element[]map(Calls a defined callback function on each element of an array, and returns an array that contains the results.
function renderComment(comment: PostComment): React.JSX.ElementrenderComment); }
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.
Define query factories
Query factoriesA query factory is a function of the query’s input that returns
queryOptions, as in TanStack Query: a key, aqueryFn, and options likestaleTime. Comments load a page at a time, socommentsQueryusesinfiniteQueryOptions.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
queryFnto the component that reads it.The input has to be serializable: it’s what a handle carries from the server to the browser.
src/queries.tsimport {function group<const TName extends string>(name: TName): Group<readonly [TName]> (+1 overload)group,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.const infiniteQueryOptions: InfiniteQueryOptionsBuilder<DefaultTypes>infiniteQueryOptions,Typed options for a paged query factory. Readers get
{ pages, pageParams }and, on their state,fetchNextPageandfetchPreviousPage:const queryOptions: QueryOptionsBuilder<DefaultTypes>queryOptions } from "@raf-query/react"; import {Typed options for a query factory:
function getPost(slug: string): Promise<Post>getPost,listComments } from "./api.ts"; constfunction listComments(input: { cursor: number; slug: string; }): Promise<CommentPage>const post: Group<readonly ["post"]>post =group<"post">(name: "post"): Group<readonly ["post"]> (+1 overload)group("post"); constA 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.const comments: Group<readonly ["comments"]>comments =group<"comments">(name: "comments"): Group<readonly ["comments"]> (+1 overload)group("comments"); export functionA 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.postQuery(function postQuery(input: { slug: string; }): QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>input: {input: { slug: string; }slug: stringslug: string }) { returnqueryOptions<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)queryOptions({Typed options for a query factory:
BaseOptions<readonly ["post", string], DefaultTypes>.queryKey: readonly ["post", string]queryKey:A plain array, usually built by a
group(). Handles keep its literal type.post(const post: Group <readonly [string]>(rest_0: string) => readonly ["post", string]input.input: { slug: string; }slug: stringslug),queryFn: (context: QueryFunctionContext<readonly ["post", string]>) => Awaitable<Post>queryFn: () =>function getPost(slug: string): Promise<Post>getPost(input.input: { slug: string; }slug: stringslug), }); } export functioncommentsQuery(function commentsQuery(input: { slug: string; }): InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>input: {input: { slug: string; }slug: stringslug: string }) { returninfiniteQueryOptions<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<...>infiniteQueryOptions({Typed options for a paged query factory. Readers get
{ pages, pageParams }and, on their state,fetchNextPageandfetchPreviousPage:BaseOptions<readonly ["comments", string], DefaultTypes>.queryKey: readonly ["comments", string]queryKey:A plain array, usually built by a
group(). Handles keep its literal type.comments(const comments: Group <readonly [string]>(rest_0: string) => readonly ["comments", string]input.input: { slug: string; }slug: stringslug),queryFn: (context: InfiniteQueryFunctionContext<readonly ["comments", string], number>) => CommentPage | PromiseLike<CommentPage>queryFn: (context: InfiniteQueryFunctionContext<readonly ["comments", string], number>context) =>listComments({function listComments(input: { cursor: number; slug: string; }): Promise<CommentPage>cursor: numbercursor:context: InfiniteQueryFunctionContext<readonly ["comments", string], number>context.InfiniteQueryFunctionContext<readonly ["comments", string], number>.pageParam: numberpageParam,slug: stringslug:input.input: { slug: string; }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, }); }Start them in a server component
Starting queriesA 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.
loadtakes named entries and starts them together. Aprefetchnever holds the response: the page streams while the post and comments load.loadreturns a handle for each entry — the query’s key and input, without the data.HydrationBoundarystreams the cache to the browser next to those handles, so nothing is fetched twice.Use
revalidatewhen the server component itself renders the data. Preferprefetchwhen a client component will read it under Suspense — the usual App Router shape, and what the example app does.src/app/(blog)/posts/[slug]/page.tsxexport default async functionPage(function Page(props: { params: Promise<{ slug: string; }>; }): Promise<JSX.Element>props: {props: { params: Promise<{ slug: string; }>; }params:params: Promise<{ slug: string; }>interface Promise<T>Promise<{Represents the completion of an asynchronous operation
slug: stringslug: string }>; }) { const {const slug: stringslug } = awaitprops.props: { params: Promise<{ slug: string; }>; }params; constparams: Promise<{ slug: string; }>qc =const qc: QueryClient<{ readonly meta: Flat<Present<FocusRefetchMeta> & Present<ReconnectRefetchMeta> & Present<PollingMeta> & Present<RetryMeta>>; }>function getQueryClient(): QueryClient<{ readonly meta: Flat<Present<FocusRefetchMeta> & Present<ReconnectRefetchMeta> & Present<PollingMeta> & Present<RetryMeta>>; }>getQueryClient(); // Never waits: both load while the page streams. const [The server components' client, one per request.
handles] = awaitconst 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>>; }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<...>load(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 everyswrandrevalidateentry has data, and rejects with the error as soon as one fails.prefetchentries never make it reject. AskipTokenin place of an entry starts nothing and gets no handle.qc, {const qc: QueryClient<{ readonly meta: Flat<Present<FocusRefetchMeta> & Present<ReconnectRefetchMeta> & Present<PollingMeta> & Present<RetryMeta>>; }>comments:comments: LoadEntry<(input: { slug: string; }) => InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>, "prefetch">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<...>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.
commentsQuery, {function commentsQuery(input: { slug: string; }): InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>slug: stringslug }),post:post: LoadEntry<(input: { slug: string; }) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>, "prefetch">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">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.
postQuery, {function postQuery(input: { slug: string; }): QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>slug: stringslug }), }); return ( <function HydrationBoundary(props: HydrationBoundaryProps): JSX.ElementHydrationBoundaryWrites server data into the browser (or SSR) client.
client: QueryClient<any>client={The request's server client (e.g. from React's
cache()). Server components only.qc}> <const qc: QueryClient<{ readonly meta: Flat<Present<FocusRefetchMeta> & Present<ReconnectRefetchMeta> & Present<PollingMeta> & Present<RetryMeta>>; }>JSX.IntrinsicElements.article: DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>article> <const Suspense: ExoticComponent<SuspenseProps>SuspenseLets you display a fallback until its children have finished loading.
SuspenseProps.fallback?: ReactNodefallback={<A fallback react tree to show when a Suspense child (like React.lazy) suspends
function PostSkeleton(): JSX.ElementPostSkeleton />}> <Postfunction Post(props: { handle: QueryHandle<typeof postQuery>; }): JSX.Elementhandle={handle: QueryHandle<(input: { slug: string; }) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>>handles.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>>; }post} /> </post: QueryHandle<(input: { slug: string; }) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>>const Suspense: ExoticComponent<SuspenseProps>Suspense> <Lets you display a fallback until its children have finished loading.
const Suspense: ExoticComponent<SuspenseProps>SuspenseLets you display a fallback until its children have finished loading.
SuspenseProps.fallback?: ReactNodefallback={<A fallback react tree to show when a Suspense child (like React.lazy) suspends
function CommentsSkeleton(): JSX.ElementCommentsSkeleton />}> <Commentsfunction Comments(props: { handle: QueryHandle<typeof commentsQuery>; }): JSX.Element[]handle={handle: QueryHandle<(input: { slug: string; }) => InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>handles.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>>; }comments} /> </comments: QueryHandle<(input: { slug: string; }) => InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>const Suspense: ExoticComponent<SuspenseProps>Suspense> </Lets you display a fallback until its children have finished loading.
JSX.IntrinsicElements.article: DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>article> </function HydrationBoundary(props: HydrationBoundaryProps): JSX.ElementHydrationBoundary> ); }Writes server data into the browser (or SSR) client.
Read them in client components
Reading queriesClient components live in their own files, marked with a
use clientdirective.useQuerytakes 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.src/app/(blog)/components/post.tsx"use client"; export functionPost(function Post(props: { handle: QueryHandle<typeof postQuery>; }): JSX.Elementprops: {props: { handle: QueryHandle<typeof postQuery>; }handle:handle: QueryHandle<(input: { slug: string; }) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>>interface QueryHandle<TFactory extends QueryFactory = (input: unknown) => { readonly queryFn: (context: never) => unknown; readonly queryKey: QueryKey; }>QueryHandle<typeofA 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.
postQuery> }) { const [postThenable] =function postQuery(input: { slug: string; }): QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>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<...>]useQuery(Read a query by its handle.
postQuery,function postQuery(input: { slug: string; }): QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>props.props: { handle: QueryHandle<typeof postQuery>; }handle); consthandle: QueryHandle<(input: { slug: string; }) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>>const post: Postpost =use<Post>(usable: Usable<Post>): Postuse(postThenable); return <const postThenable: QueryValue<(input: { slug: string; }) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>>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 functionComments(function Comments(props: { handle: QueryHandle<typeof commentsQuery>; }): JSX.Element[]props: {props: { handle: QueryHandle<typeof commentsQuery>; }handle:handle: QueryHandle<(input: { slug: string; }) => InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>interface QueryHandle<TFactory extends QueryFactory = (input: unknown) => { readonly queryFn: (context: never) => unknown; readonly queryKey: QueryKey; }>QueryHandle<typeofA 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.
commentsQuery> }) { // May still be loading. If so, use() suspends until it lands. const [function commentsQuery(input: { slug: string; }): InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>pagesThenable] =const pagesThenable: QueryValue<(input: { slug: string; }) => InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>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 [...]useInfiniteQuery(Read a paged query (
infiniteQueryOptions) by its handle. The data is{ pages, pageParams }, and its state loads more pages.commentsQuery,function commentsQuery(input: { slug: string; }): InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>props.props: { handle: QueryHandle<typeof commentsQuery>; }handle); const pages =handle: QueryHandle<(input: { slug: string; }) => InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>use<InfiniteData<CommentPage, number>>(usable: Usable<InfiniteData<CommentPage, number>>): InfiniteData<CommentPage, number>use(pagesThenable); returnconst pagesThenable: QueryValue<(input: { slug: string; }) => InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>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[]flatMap((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.
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[]map((Calls a defined callback function on each element of an array, and returns an array that contains the results.
comment: PostCommentcomment) => <Commentfunction Comment(props: { comment: PostComment; }): React.ReactNodeAttributes.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.
Define query factories
Query factoriesA query factory is a function of the query’s input that returns
queryOptions, as in TanStack Query: a key, aqueryFn, and options likestaleTime. Comments load a page at a time, socommentsQueryusesinfiniteQueryOptions.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
queryFnto the component that reads it.The input has to be serializable: it’s what a handle carries from the server to the browser.
app/queries.tsimport {function group<const TName extends string>(name: TName): Group<readonly [TName]> (+1 overload)group,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.const infiniteQueryOptions: InfiniteQueryOptionsBuilder<DefaultTypes>infiniteQueryOptions,Typed options for a paged query factory. Readers get
{ pages, pageParams }and, on their state,fetchNextPageandfetchPreviousPage:const queryOptions: QueryOptionsBuilder<DefaultTypes>queryOptions } from "@raf-query/react"; import {Typed options for a query factory:
function getPost(slug: string): Promise<Post>getPost,listComments } from "./api.ts"; constfunction listComments(input: { cursor: number; slug: string; }): Promise<CommentPage>const post: Group<readonly ["post"]>post =group<"post">(name: "post"): Group<readonly ["post"]> (+1 overload)group("post"); constA 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.const comments: Group<readonly ["comments"]>comments =group<"comments">(name: "comments"): Group<readonly ["comments"]> (+1 overload)group("comments"); export functionA 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.postQuery(function postQuery(input: { slug: string; }): QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>input: {input: { slug: string; }slug: stringslug: string }) { returnqueryOptions<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)queryOptions({Typed options for a query factory:
BaseOptions<readonly ["post", string], DefaultTypes>.queryKey: readonly ["post", string]queryKey:A plain array, usually built by a
group(). Handles keep its literal type.post(const post: Group <readonly [string]>(rest_0: string) => readonly ["post", string]input.input: { slug: string; }slug: stringslug),queryFn: (context: QueryFunctionContext<readonly ["post", string]>) => Awaitable<Post>queryFn: () =>function getPost(slug: string): Promise<Post>getPost(input.input: { slug: string; }slug: stringslug), }); } export functioncommentsQuery(function commentsQuery(input: { slug: string; }): InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>input: {input: { slug: string; }slug: stringslug: string }) { returninfiniteQueryOptions<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<...>infiniteQueryOptions({Typed options for a paged query factory. Readers get
{ pages, pageParams }and, on their state,fetchNextPageandfetchPreviousPage:BaseOptions<readonly ["comments", string], DefaultTypes>.queryKey: readonly ["comments", string]queryKey:A plain array, usually built by a
group(). Handles keep its literal type.comments(const comments: Group <readonly [string]>(rest_0: string) => readonly ["comments", string]input.input: { slug: string; }slug: stringslug),queryFn: (context: InfiniteQueryFunctionContext<readonly ["comments", string], number>) => CommentPage | PromiseLike<CommentPage>queryFn: (context: InfiniteQueryFunctionContext<readonly ["comments", string], number>context) =>listComments({function listComments(input: { cursor: number; slug: string; }): Promise<CommentPage>cursor: numbercursor:context: InfiniteQueryFunctionContext<readonly ["comments", string], number>context.InfiniteQueryFunctionContext<readonly ["comments", string], number>.pageParam: numberpageParam,slug: stringslug:input.input: { slug: string; }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, }); }Start them in the route loader
React RouterReact 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.
loadtakes named entries and starts them together. Aprefetchnever holds the navigation: the page renders while the post and comments load.loadreturns a handle for each entry — the query’s key and input, without the data. The loader returns the handles withdehydrate(qc), and React Router streams the queries still loading to the browser, so nothing is fetched twice.Use
swrwhen the route can’t lay out without the data. Preferprefetchwhen a component will read it under Suspense — the usual shape, and what the example app does for streamed parts of the page.app/routes/blog.$slug.tsxexport async functionloader(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>>; }>args: Route.args: CreateServerLoaderArgs<Info & { module: typeof import("./blog.$slug"); matches: Matches; }>LoaderArgs) { consttype Route.LoaderArgs = { request: Request; url: URL; params: { slug: string; }; pattern: string; context: Readonly<RouterContextProvider>; }const qc: QueryClient<any>qc =args.args: CreateServerLoaderArgs<Info & { module: typeof import("./blog.$slug"); matches: Matches; }>context: Readonly<RouterContextProvider>context.An instance of
RouterContextProviderthat 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 yourgetLoadContextfunction.get<QueryClient<any>>(context: RouterContext<QueryClient<any>>): QueryClient<any>get(Access a value from the context. If no value has been set for the context, it will return the context's
defaultValueif provided, or throw an error if nodefaultValuewas set.const queryClientContext: RouterContext<QueryClient<any>>queryClientContext); const {const slug: stringslug } =args.args: CreateServerLoaderArgs<Info & { module: typeof import("./blog.$slug"); matches: Matches; }>params: { slug: string; }params; // Never waits: both load while the page renders. const [Dynamic route params for the current route.
handles] = awaitconst 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>>; }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<...>load(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 everyswrandrevalidateentry has data, and rejects with the error as soon as one fails.prefetchentries never make it reject. AskipTokenin place of an entry starts nothing and gets no handle.const qc: QueryClient<any>qc, {comments:comments: LoadEntry<(input: { slug: string; }) => InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>, "prefetch">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<...>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.
commentsQuery, {function commentsQuery(input: { slug: string; }): InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>slug: stringslug }),post:post: LoadEntry<(input: { slug: string; }) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>, "prefetch">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">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.
postQuery, {function postQuery(input: { slug: string; }): QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>slug: stringslug }), }); return { ...handles,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>>; }dehydratedState: DehydratedStatedehydratedState:function dehydrate(client: QueryClient, options?: DehydrateOptions): DehydratedStatedehydrate(Snapshot the client's entries for transfer.
const qc: QueryClient<any>qc) }; }Read them in components
Reading queriesThe route component wraps the page in
HydrationBoundary, which writes the loader’s snapshot into the browser’s cache.useQuerytakes 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.app/routes/blog.$slug.tsxexport default functionfunction PostPage(props: Route.ComponentProps): React.JSX.ElementPostPage(props: Route.props: CreateComponentProps<Info & { module: typeof import("./blog.$slug"); matches: Matches; }, false>ComponentProps) { const {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)[]]; } & { ...; }comments,const comments: { readonly input: { slug: string; }; readonly queryKey: ["comments", string]; }dehydratedState,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; }[]; }post } =const post: { readonly input: { slug: string; }; readonly queryKey: ["post", string]; }props.props: CreateComponentProps<Info & { module: typeof import("./blog.$slug"); matches: Matches; }, false>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: { ...; }; }loaderData; return ( <The data returned from the
loaderorclientLoaderfunction HydrationBoundary(props: HydrationBoundaryProps): React.JSX.ElementHydrationBoundaryWrites server data into the browser (or SSR) client.
state: DehydratedStatestate={A loader's
dehydrate(client).dehydratedState}> <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; }[]; }React.JSX.IntrinsicElements.article: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>article> <const Suspense: React.ExoticComponent<React.SuspenseProps>SuspenseLets you display a fallback until its children have finished loading.
React.SuspenseProps.fallback?: React.ReactNodefallback={<A fallback react tree to show when a Suspense child (like React.lazy) suspends
function PostSkeleton(): React.ReactNodePostSkeleton />}> <Postfunction Post(props: { handle: QueryHandle<typeof postQuery>; }): React.JSX.Elementhandle={handle: QueryHandle<(input: { slug: string; }) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>>post} /> </const post: { readonly input: { slug: string; }; readonly queryKey: ["post", string]; }const Suspense: React.ExoticComponent<React.SuspenseProps>Suspense> <Lets you display a fallback until its children have finished loading.
const Suspense: React.ExoticComponent<React.SuspenseProps>SuspenseLets you display a fallback until its children have finished loading.
React.SuspenseProps.fallback?: React.ReactNodefallback={<A fallback react tree to show when a Suspense child (like React.lazy) suspends
function CommentsSkeleton(): React.JSX.ElementCommentsSkeleton />}> <PostCommentsfunction PostComments(props: { handle: QueryHandle<typeof commentsQuery>; }): React.JSX.Element[]handle={handle: QueryHandle<(input: { slug: string; }) => InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>comments} /> </const comments: { readonly input: { slug: string; }; readonly queryKey: ["comments", string]; }const Suspense: React.ExoticComponent<React.SuspenseProps>Suspense> </Lets you display a fallback until its children have finished loading.
React.JSX.IntrinsicElements.article: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>article> </function HydrationBoundary(props: HydrationBoundaryProps): React.JSX.ElementHydrationBoundary> ); } functionWrites server data into the browser (or SSR) client.
Post(function Post(props: { handle: QueryHandle<typeof postQuery>; }): React.JSX.Elementprops: {props: { handle: QueryHandle<typeof postQuery>; }handle:handle: QueryHandle<(input: { slug: string; }) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>>interface QueryHandle<TFactory extends QueryFactory = (input: unknown) => { readonly queryFn: (context: never) => unknown; readonly queryKey: QueryKey; }>QueryHandle<typeofA 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.
postQuery> }) { const [postThenable] =function postQuery(input: { slug: string; }): QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>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<...>]useQuery(Read a query by its handle.
postQuery,function postQuery(input: { slug: string; }): QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>props.props: { handle: QueryHandle<typeof postQuery>; }handle); consthandle: QueryHandle<(input: { slug: string; }) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>>const post: Postpost =use<Post>(usable: React.Usable<Post>): Postuse(postThenable); return <const postThenable: QueryValue<(input: { slug: string; }) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>>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>; } functionPostComments(function PostComments(props: { handle: QueryHandle<typeof commentsQuery>; }): React.JSX.Element[]props: {props: { handle: QueryHandle<typeof commentsQuery>; }handle:handle: QueryHandle<(input: { slug: string; }) => InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>interface QueryHandle<TFactory extends QueryFactory = (input: unknown) => { readonly queryFn: (context: never) => unknown; readonly queryKey: QueryKey; }>QueryHandle<typeofA 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.
commentsQuery> }) { // May still be loading. If so, use() suspends until it lands. const [function commentsQuery(input: { slug: string; }): InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>pagesThenable] =const pagesThenable: QueryValue<(input: { slug: string; }) => InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>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 [...]useInfiniteQuery(Read a paged query (
infiniteQueryOptions) by its handle. The data is{ pages, pageParams }, and its state loads more pages.commentsQuery,function commentsQuery(input: { slug: string; }): InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>props.props: { handle: QueryHandle<typeof commentsQuery>; }handle); consthandle: QueryHandle<(input: { slug: string; }) => InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>const pages: InfiniteData<CommentPage, number>pages =use<InfiniteData<CommentPage, number>>(usable: React.Usable<InfiniteData<CommentPage, number>>): InfiniteData<CommentPage, number>use(pagesThenable); returnconst pagesThenable: QueryValue<(input: { slug: string; }) => InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>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[]flatMap((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.
page: CommentPagepage) =>page: CommentPagepage.CommentPage.items: readonly PostComment[]items) .Array<PostComment>.map<React.JSX.Element>(callbackfn: (value: PostComment, index: number, array: PostComment[]) => React.JSX.Element, thisArg?: any): React.JSX.Element[]map((Calls a defined callback function on each element of an array, and returns an array that contains the results.
comment: PostCommentcomment) => <Commentfunction Comment(props: { comment: PostComment; }): React.ReactNodeReact.Attributes.key?: React.Key | null | undefinedkey={comment: PostCommentcomment.PostComment.id: stringid}comment: PostCommentcomment={comment: PostCommentcomment} />); }
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.
- Fetch where React recommendsRoute loaders and server components start every query a page needs, in parallel, before anything renders.
- Small, serializable handlesA handle is a query's key and input. It crosses the loader, the server and the client, and the cache carries the data next to it.
- Thenables in, use() outuseQuery returns a stable thenable for every handle; read it with use() wherever the data is needed, even in a child.
- The URL is the inputFilters, sort and pages live in the URL. whilePending picks between suspending and keeping the previous page.
- Actions for writesNo useMutation: wrap a write in action() and name the queries it invalidates. React's useOptimistic and useActionState do the rest.
- Streaming SSR and RSCThe server's cache streams to the browser as queries settle, in TanStack Start, React Router and Next.js App Router.
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 Query | raf-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: keepPreviousData | whilePending: "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.