Skip to content

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

raf-query

Integrations

React Server Components (Next.js)

Start queries in server components, send them to client components with HydrationBoundary, and refetch from the browser. For the Next.js App Router and other RSC frameworks.

With React Server Components, server components play the part of loaders: they start queries and pass handles to client components. QueryClientProvider and HydrationBoundary, both from @raf-query/react, connect them.

The samples use the Next.js App Router. Other RSC frameworks work the same way: the integration needs only a bundler that resolves the react-server export condition, which React itself needs too. It's tested on Vite's RSC plugin, which Waku and React Router's RSC mode build on. Navigation, request headers and server actions are the framework's own, so those sections show the Next.js APIs.

Setup

One function makes the app's clients. Server components get one per request, scoped by React's cache():

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

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

createQueryClient
, function recommended(): Recommended

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

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

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

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

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

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

Default staleTime: 0.

staleTime
: 5000 });
} /** The server components' client, one per request. */ export const
const getQueryClient: () => QueryClient<{
    readonly meta: Flat<Present<FocusRefetchMeta> & Present<ReconnectRefetchMeta> & Present<PollingMeta> & Present<RetryMeta>>;
}>

The server components' client, one per request.

getQueryClient
=
cache<() => QueryClient<{
    readonly meta: Flat<Present<FocusRefetchMeta> & Present<ReconnectRefetchMeta> & Present<PollingMeta> & Present<RetryMeta>>;
}>>(fn: () => QueryClient<{
    readonly meta: Flat<Present<FocusRefetchMeta> & Present<ReconnectRefetchMeta> & Present<PollingMeta> & Present<RetryMeta>>;
}>): () => QueryClient<{
    readonly meta: Flat<Present<FocusRefetchMeta> & Present<ReconnectRefetchMeta> & Present<PollingMeta> & Present<RetryMeta>>;
}>
cache
(
function makeQueryClient(): QueryClient<{
    readonly meta: Flat<Present<FocusRefetchMeta> & Present<ReconnectRefetchMeta> & Present<PollingMeta> & Present<RetryMeta>>;
}>
makeQueryClient
);

Client components get theirs from a Providers client component, which creates one client per server render and one per browser tab. Links and features, such as the ones recommended() returns, are functions, which a server component can't pass to a client component, so the client is made on the client side of that boundary. The same component works in every framework:

src/components/providers.tsx
"use client";

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

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

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

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

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

Represents all of the things React can render.

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

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

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

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

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

Represents all of the things React can render.

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

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

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

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

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

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

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

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

QueryClientProvider
>
); }

And render that in the root layout:

src/app/layout.tsx
import { function QueryDevtools(props: QueryDevtoolsProps): JSX.Element

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

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

Represents all of the things React can render.

ReactNode
} from "react";
import {
function Providers(props: {
    readonly children: ReactNode;
    readonly client?: QueryClient;
}): JSX.Element

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

Providers
} from "~/components/providers.tsx";
export default function
function RootLayout(props: {
    children: ReactNode;
}): JSX.Element
RootLayout
(
props: {
    children: ReactNode;
}
props
: { children: ReactNodechildren: type ReactNode = string | number | bigint | boolean | ReactElement<unknown, string | JSXElementConstructor<any>> | Iterable<ReactNode> | ReactPortal | Promise<AwaitedReactNode> | null | undefined

Represents all of the things React can render.

ReactNode
}) {
return ( <JSX.IntrinsicElements.html: DetailedHTMLProps<HtmlHTMLAttributes<HTMLHtmlElement>, HTMLHtmlElement>html HTMLAttributes<HTMLHtmlElement>.lang?: string | undefinedlang="en"> <JSX.IntrinsicElements.body: DetailedHTMLProps<HTMLAttributes<HTMLBodyElement>, HTMLBodyElement>body> <
function Providers(props: {
    readonly children: ReactNode;
    readonly client?: QueryClient;
}): JSX.Element

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

Providers
>
{
props: {
    children: ReactNode;
}
props
.children: ReactNodechildren}
{var process: NodeJS.Processprocess.NodeJS.Process.env: NodeJS.ProcessEnv

The process.env property returns an object containing the user environment. See environ(7).

env
.string | undefinedNODE_ENV !== "production" ? <function QueryDevtools(props: QueryDevtoolsProps): JSX.Element

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

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

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

Providers
>
</JSX.IntrinsicElements.body: DetailedHTMLProps<HTMLAttributes<HTMLBodyElement>, HTMLBodyElement>body> </JSX.IntrinsicElements.html: DetailedHTMLProps<HtmlHTMLAttributes<HTMLHtmlElement>, HTMLHtmlElement>html> ); }

Mount DevTools only when process.env.NODE_ENV !== "production" so the bundler drops the panel from production builds. The example app always renders it, so the panel stays available in the deployed demo.

Pages

A server component starts queries with load, and wraps what it renders in a HydrationBoundary that sends the request client's entries to the browser:

src/app/(blog)/posts/[slug]/page.tsx
import { function HydrationBoundary(props: HydrationBoundaryProps): JSX.Element

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

HydrationBoundary
, function load<const TEntries extends LoadEntries>(client: QueryClient, entries: TEntries): Promise<Loaded<TEntries>>

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
, function prefetch<TFactory extends QueryFactory>(factory: TFactory, ...args: LoadEntryArgs<TFactory>): LoadEntry<TFactory, "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
} from "@raf-query/react";
import {
function commentsQuery(input: {
    slug: string;
}): InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>
commentsQuery
,
function postQuery(input: {
    slug: string;
}): QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>
postQuery
} from "~/queries.ts";
import {
const getQueryClient: () => QueryClient<{
    readonly meta: Flat<Present<FocusRefetchMeta> & Present<ReconnectRefetchMeta> & Present<PollingMeta> & Present<RetryMeta>>;
}>

The server components' client, one per request.

getQueryClient
} from "~/query-client.ts";
export default async function
function Page(props: {
    params: Promise<{
        slug: string;
    }>;
}): Promise<JSX.Element>
Page
(
props: {
    params: Promise<{
        slug: string;
    }>;
}
props
: {
params: Promise<{
    slug: string;
}>
params
: interface Promise<T>

Represents the completion of an asynchronous operation

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

The server components' client, one per request.

getQueryClient
();
// Never waits: both load while the page streams. const [
const handles: {
    readonly comments: QueryHandle<(input: {
        slug: string;
    }) => InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>;
    readonly post: QueryHandle<(input: {
        slug: string;
    }) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>>;
}
handles
] = await
load<{
    readonly comments: LoadEntry<(input: {
        slug: string;
    }) => InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>, "prefetch">;
    readonly post: LoadEntry<(input: {
        slug: string;
    }) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>, "prefetch">;
}>(client: QueryClient, entries: {
    readonly comments: LoadEntry<(input: {
        slug: string;
    }) => InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>, "prefetch">;
    readonly post: LoadEntry<(input: {
        slug: string;
    }) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>, "prefetch">;
}): Promise<...>

Start a loader's queries and return a serializable handle for each. It starts every entry before it waits for any, so the order of the entries can't create a waterfall. Resolves with [handles, data] once every swr and revalidate entry has data, and rejects with the error as soon as one fails. prefetch entries never make it reject. A skipToken in place of an entry starts nothing and gets no handle.

load
(
const qc: QueryClient<{
    readonly meta: Flat<Present<FocusRefetchMeta> & Present<ReconnectRefetchMeta> & Present<PollingMeta> & Present<RetryMeta>>;
}>
qc
, {
comments: LoadEntry<(input: {
    slug: string;
}) => InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>, "prefetch">
comments
:
prefetch<(input: {
    slug: string;
}) => InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>(factory: (input: {
    slug: string;
}) => InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>, ...args: LoadEntryArgs<(input: {
    slug: string;
}) => InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>): LoadEntry<...>

Start the query if the cache has no fresh data, and don't wait for it. It never makes the load reject: errors show up where a component reads the handle.

prefetch
(
function commentsQuery(input: {
    slug: string;
}): InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>
commentsQuery
, { slug: stringslug }),
post: LoadEntry<(input: {
    slug: string;
}) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>, "prefetch">
post
:
prefetch<(input: {
    slug: string;
}) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>>(factory: (input: {
    slug: string;
}) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>, ...args: LoadEntryArgs<(input: {
    slug: string;
}) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>>): LoadEntry<(input: {
    slug: string;
}) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>, "prefetch">

Start the query if the cache has no fresh data, and don't wait for it. It never makes the load reject: errors show up where a component reads the handle.

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

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

HydrationBoundary
client: QueryClient<any>

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

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

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

Suspense
SuspenseProps.fallback?: ReactNode

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

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

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

Suspense
>
<const Suspense: ExoticComponent<SuspenseProps>

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

Suspense
SuspenseProps.fallback?: ReactNode

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

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

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

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

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

HydrationBoundary
>
); }
src/app/(blog)/components/post.tsx
"use client";
export function 
function Post(props: {
    handle: QueryHandle<typeof postQuery>;
}): JSX.Element
Post
(
props: {
    handle: QueryHandle<typeof postQuery>;
}
props
: {
handle: QueryHandle<(input: {
    slug: string;
}) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>>
handle
: interface QueryHandle<TFactory extends QueryFactory = (input: unknown) => { readonly queryFn: (context: never) => unknown; readonly queryKey: QueryKey; }>

A small serializable reference to a query: its key and its factory's input. It carries no data; readers get that from the cache. Handles of different factories have different types.

QueryHandle
<typeof
function postQuery(input: {
    slug: string;
}): QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>
postQuery
> }) {
const [postThenable] =
useQuery<(input: {
    slug: string;
}) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>>(factory: (input: {
    slug: string;
}) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>, handle: QueryHandle<(input: {
    slug: string;
}) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>>, options?: UseQueryOptions): readonly [QueryValue<(input: {
    slug: string;
}) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>>, QueryState<...>]

Read a query by its handle.

useQuery
(
function postQuery(input: {
    slug: string;
}): QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>
postQuery
,
props: {
    handle: QueryHandle<typeof postQuery>;
}
props
.
handle: QueryHandle<(input: {
    slug: string;
}) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>>
handle
);
const postThenable: QueryValue<(input: {
    slug: string;
}) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>>
const const post: Postpost = use<Post>(usable: Usable<Post>): Postuse(
const postThenable: QueryValue<(input: {
    slug: string;
}) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>>
postThenable
);
return <JSX.IntrinsicElements.h1: DetailedHTMLProps<HTMLAttributes<HTMLHeadingElement>, HTMLHeadingElement>h1>{const post: Postpost.Post.title: stringtitle}</JSX.IntrinsicElements.h1: DetailedHTMLProps<HTMLAttributes<HTMLHeadingElement>, HTMLHeadingElement>h1>; }
src/app/(blog)/components/comments.tsx
"use client";
export function 
function Comments(props: {
    handle: QueryHandle<typeof commentsQuery>;
}): JSX.Element[]
Comments
(
props: {
    handle: QueryHandle<typeof commentsQuery>;
}
props
: {
handle: QueryHandle<(input: {
    slug: string;
}) => InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>
handle
: interface QueryHandle<TFactory extends QueryFactory = (input: unknown) => { readonly queryFn: (context: never) => unknown; readonly queryKey: QueryKey; }>

A small serializable reference to a query: its key and its factory's input. It carries no data; readers get that from the cache. Handles of different factories have different types.

QueryHandle
<typeof
function commentsQuery(input: {
    slug: string;
}): InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>
commentsQuery
> }) {
// May still be loading. If so, use() suspends until it lands. const [
const pagesThenable: QueryValue<(input: {
    slug: string;
}) => InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>
pagesThenable
] =
useInfiniteQuery<(input: {
    slug: string;
}) => InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>(factory: (input: {
    slug: string;
}) => InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>, handle: QueryHandle<(input: {
    slug: string;
}) => InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>, options?: UseQueryOptions): readonly [...]

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

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

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

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

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

@paramcallbackfn — A function that accepts up to three arguments. The map method calls the callbackfn function one time for each element in the array.@paramthisArg — An object to which the this keyword can refer in the callbackfn function. If thisArg is omitted, undefined is used as the this value.
map
((comment: PostCommentcomment) => <
function Comment(props: {
    comment: PostComment;
}): React.ReactNode
Comment
Attributes.key?: Key | null | undefinedkey={comment: PostCommentcomment.PostComment.id: stringid} comment: PostCommentcomment={comment: PostCommentcomment} />);
}
  • Render a HydrationBoundary in each server component that starts queries for client components. Entries already sent in this request are skipped, so nesting them is fine.
  • client works only in server components. The bundler resolves @raf-query/react with the react-server export condition there, which picks the HydrationBoundary that dehydrates the client. Client components get the one that takes a dehydrated state, as in React Router.
  • Pending entries stream as promises through the RSC payload: the page's shell renders while comments loads.
  • options takes dehydrate's options, such as keepErrors and filter.
  • Use prefetch and revalidate in server components, not swr. Each request's client starts empty, so swr waits for the request every time, exactly like revalidate. See Starting queries.

Data in the server component

To render a query's data in the server component itself, wait for it with revalidate. load resolves to the handles and the data of the entries it waited for:

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
();
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
,
const data: LoadedData<{
    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>, "revalidate">;
}>
data
] = 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>, "revalidate">;
}>(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>, "revalidate">;
}): 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 }),
// Waits, so the server component can render the post.
post: LoadEntry<(input: {
    slug: string;
}) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>, "revalidate">
post
:
revalidate<(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>, "revalidate">

Wait for fresh data, as HTTP's must-revalidate does. Fresh cached data resolves at once without a refetch (staleTime is the max-age); with stale data or none it waits for the fetch. On a server client, data counts as fresh until a write invalidates it. The load rejects if the fetch fails.

revalidate
(
function postQuery(input: {
    slug: string;
}): QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>
postQuery
, { slug: stringslug }),
}); <function HydrationBoundary(props: HydrationBoundaryProps): JSX.Element

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

HydrationBoundary
client: QueryClient<any>

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

client
={
const qc: QueryClient<{
    readonly meta: Flat<Present<FocusRefetchMeta> & Present<ReconnectRefetchMeta> & Present<PollingMeta> & Present<RetryMeta>>;
}>
qc
}>
<JSX.IntrinsicElements.title: DetailedHTMLProps<HTMLAttributes<HTMLTitleElement>, HTMLTitleElement>title>{
const data: LoadedData<{
    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>, "revalidate">;
}>
data
.post: Postpost.Post.title: stringtitle}</JSX.IntrinsicElements.title: DetailedHTMLProps<HTMLAttributes<HTMLTitleElement>, HTMLTitleElement>title>
<
function PostTitle(props: {
    handle: QueryHandle<typeof postQuery>;
}): JSX.Element
PostTitle
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
SuspenseProps.fallback?: ReactNode

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

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

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

Suspense
>
</function HydrationBoundary(props: HydrationBoundaryProps): JSX.Element

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

HydrationBoundary
>;

handles.post came from a revalidate entry: a client component reads it with use() without suspending, and follows later refetches. The handle carries no data: the post crosses once, in the HydrationBoundary's entries.

src/app/(blog)/components/post-title.tsx
"use client";
export function 
function PostTitle(props: {
    handle: QueryHandle<typeof postQuery>;
}): JSX.Element
PostTitle
(
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
> }) {
// The server component waited for the post: this is the post, not a promise. const [
const postThenable: QueryValue<(input: {
    slug: string;
}) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>>
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 post = use<Post>(usable: Usable<Post>): Postuse(
const postThenable: QueryValue<(input: {
    slug: string;
}) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>>
postThenable
);
const post: Post
return <JSX.IntrinsicElements.h1: DetailedHTMLProps<HTMLAttributes<HTMLHeadingElement>, HTMLHeadingElement>h1>{const post: Postpost.Post.title: stringtitle}</JSX.IntrinsicElements.h1: DetailedHTMLProps<HTMLAttributes<HTMLHeadingElement>, HTMLHeadingElement>h1>; }

Pending UI

While a link navigation is loading the next RSC payload, show pending state with the framework's router — not useIsFetching. In Next.js, render useLinkStatus as a child of the Link it tracks:

"use client";

function function PendingHint(): JSX.Element | nullPendingHint() {
	const { const pending: booleanpending } = 
function useLinkStatus(): {
    readonly pending: boolean;
}
useLinkStatus
();
return const pending: booleanpending ? <function Spinner(): ReactNodeSpinner /> : null; } <
function Link(props: {
    readonly children?: ReactNode;
    readonly href: string;
}): ReactNode
Link
href: stringhref="/issues">
Issues <function PendingHint(): JSX.Element | nullPendingHint /> </
function Link(props: {
    readonly children?: ReactNode;
    readonly href: string;
}): ReactNode
Link
>;

Per-query background refetches after the page has rendered use isFetching on the query's state. See Background fetching indicators.

Search params

Read searchParams in the page, validate them, and pass them as the input:

src/app/issues/page.tsx
export default async function 
function IssuesPage(props: {
    searchParams: Promise<Record<string, string | Array<string>>>;
}): Promise<null>
IssuesPage
(
props: {
    searchParams: Promise<Record<string, string | Array<string>>>;
}
props
: {
searchParams: Promise<Record<string, string | string[]>>searchParams: interface Promise<T>

Represents the completion of an asynchronous operation

Promise
<type Record<K extends keyof any, T> = { [P in K]: T; }

Construct a type with a set of properties K of type T

Record
<string, string | interface Array<T>Array<string>>>;
}) { const const input: IssueListFilterinput = const IssueListInput: IssueListParserIssueListInput.IssueListParser.parse(value: Record<string, string | Array<string>>): IssueListFilterparse(await
props: {
    searchParams: Promise<Record<string, string | Array<string>>>;
}
props
.searchParams: Promise<Record<string, string | string[]>>searchParams);
const [
const handles: {
    readonly issues: QueryHandle<(input: {
        status: "open" | "done";
    }) => QueryOptions<readonly ["issues", {
        status: "open" | "done";
    }], Issue[], Issue[], DefaultTypes>>;
}
handles
] = await
load<{
    readonly issues: LoadEntry<(input: {
        status: "open" | "done";
    }) => QueryOptions<readonly ["issues", {
        status: "open" | "done";
    }], Issue[], Issue[], DefaultTypes>, "prefetch">;
}>(client: QueryClient, entries: {
    readonly issues: LoadEntry<(input: {
        status: "open" | "done";
    }) => QueryOptions<readonly ["issues", {
        status: "open" | "done";
    }], Issue[], Issue[], DefaultTypes>, "prefetch">;
}): Promise<Loaded<{
    readonly issues: LoadEntry<(input: {
        status: "open" | "done";
    }) => QueryOptions<readonly ["issues", {
        status: "open" | "done";
    }], Issue[], Issue[], DefaultTypes>, "prefetch">;
}>>

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
(
function getQueryClient(): QueryClient<{
    readonly meta: Flat<Present<FocusRefetchMeta> & Present<ReconnectRefetchMeta> & Present<PollingMeta> & Present<RetryMeta>>;
}>

The server components' client, one per request.

getQueryClient
(), {
issues: LoadEntry<(input: {
    status: "open" | "done";
}) => QueryOptions<readonly ["issues", {
    status: "open" | "done";
}], Issue[], Issue[], DefaultTypes>, "prefetch">
issues
:
prefetch<(input: {
    status: "open" | "done";
}) => QueryOptions<readonly ["issues", {
    status: "open" | "done";
}], Issue[], Issue[], DefaultTypes>>(factory: (input: {
    status: "open" | "done";
}) => QueryOptions<readonly ["issues", {
    status: "open" | "done";
}], Issue[], Issue[], DefaultTypes>, ...args: LoadEntryArgs<(input: {
    status: "open" | "done";
}) => QueryOptions<readonly ["issues", {
    status: "open" | "done";
}], Issue[], Issue[], 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 issueListQuery(input: {
    status: "open" | "done";
}): QueryOptions<readonly ["issues", {
    status: "open" | "done";
}], Issue[], Issue[], DefaultTypes>
issueListQuery
, const input: IssueListFilterinput),
}); // ...

Changing search params with <Link> or router.push renders the page again on the server with the new input. See URL state and pagination for what to show while it loads.

Queries that start in the browser

On the server, only server components start requests. HydrationBoundary marks the client components' server render as it renders: below it, a client component reading a query that no HydrationBoundary sent renders its Suspense fallback on the server, and the query loads in the browser. This is what happens when a client component reads a handle from waterfallHandle. That boundary renders in the browser through React's error recovery, with the digest Next.js uses for ssr: false, so Next.js doesn't report it. Other RSC frameworks see a recoverable error.

A client component that renders before the first HydrationBoundary, such as one in the root layout around {children}, isn't below a mark yet: a query it reads that nothing started fetches during the server render, and the browser fetches it again. See SSR and hydration.

useQuery, useInfiniteQuery, useIsFetching and useQueryClient read the client's cache, so they belong in client components. Called in a server component, each throws an error that says so.

Server and browser requests

A factory's queryFn runs in two places, and has to reach the same data as the same user in both:

Server componentsBrowser
WhenThe page requestRefetches and later pages
fetch("/api/...")Fails: there is no page origin to resolve itWorks
The user's cookiesYours to forward, from headers()Sent by the browser

Below a HydrationBoundary, client components' server render starts no request, so it isn't a third place. A client component above every boundary that reads a query nothing started is one: keep such reads below the page's boundary.

An API in the same app

Have the API read the session from the request's cookies, and give factories one apiFetch with two files behind it: on the server it calls the API's handler in the process with the page request's cookies, and in the browser it's fetch. A subpath import picks the file. The bundler resolves the react-server condition in server components, so the server file can import the request's headers (next/headers in Next.js) and your API, and neither reaches the browser bundle:

package.json
{
	"imports": {
		"#api-fetch": {
			"react-server": "./src/api-fetch.server.ts",
			"default": "./src/api-fetch.ts"
		}
	}
}
src/api-fetch.server.ts
import { function headers(): Promise<ReadonlyHeaders>

This function allows you to read the HTTP incoming request headers in Server Components, Server Actions, Route Handlers and Middleware.

headers
} from "next/headers";
// The function `app/api/[...path]/route.ts` exports as `GET` and `POST`. import { function handler(_request: Request): Promise<Response>handler } from "./api-handler.ts"; export async function function apiFetch(path: string, init?: RequestInit): Promise<Response>apiFetch(path: stringpath: string, init: RequestInit | undefinedinit?: RequestInit) { const const forwarded: Headersforwarded = new var Headers: new (init?: HeadersInit) => Headers

The Headers interface of the Fetch API allows you to perform various actions on HTTP request and response headers. These actions include retrieving, setting, adding to, and removing headers from the list of the request's headers.

Headers
(init: RequestInit | undefinedinit?.RequestInit.headers?: HeadersInit | undefined

A Headers object, an object literal, or an array of two-item arrays to set request's headers.

headers
);
const const cookie: string | nullcookie = (await function headers(): Promise<ReadonlyHeaders>

This function allows you to read the HTTP incoming request headers in Server Components, Server Actions, Route Handlers and Middleware.

headers
()).Headers.get(name: string): string | null

The get() method of the Headers interface returns a byte string of all the values of a header within a Headers object with a given name. If the requested header doesn't exist in the Headers object, it returns null.

get
("cookie");
if (const cookie: string | nullcookie !== null) { const forwarded: Headersforwarded.Headers.set(name: string, value: string): void

The set() method of the Headers interface sets a new value for an existing header inside a Headers object, or adds the header if it does not already exist.

set
("cookie", const cookie: stringcookie);
} // The host is a placeholder: the handler routes by path. const const url: URLurl = new var URL: new (url: string | URL, base?: string | URL) => URL

The URL interface is used to parse, construct, normalize, and encode URLs. It works by providing properties which allow you to easily read and modify the components of a URL.

URL
(path: stringpath, "http://localhost");
return function handler(_request: Request): Promise<Response>handler(new var Request: new (input: RequestInfo | URL, init?: RequestInit) => Request

The Request interface of the Fetch API represents a resource request.

Request
(const url: URLurl, { ...init: RequestInit | undefinedinit, RequestInit.headers?: HeadersInit | undefined

A Headers object, an object literal, or an array of two-item arrays to set request's headers.

headers
: const forwarded: Headersforwarded }));
}
src/api-fetch.ts
// The browser sends the page's cookies to its own origin.
export function function apiFetch(path: string, init?: RequestInit): Promise<Response>apiFetch(path: stringpath: string, init: RequestInit | undefinedinit?: RequestInit) {
	return function fetch(input: string | URL | Request, init?: RequestInit): Promise<Response> (+1 overload)fetch(path: stringpath, init: RequestInit | undefinedinit);
}
src/queries.ts
import { function apiFetch(path: string, init?: RequestInit): Promise<Response>apiFetch } from "#api-fetch";

const const issue: Group<readonly ["issue"]>issue = group<"issue">(name: "issue"): Group<readonly ["issue"]> (+1 overload)

A group, or a subgroup inside parent. Call it for a query's key: a flat, plain array that starts with the group's path. Invalidating a group covers its subgroups and every query below them.

group
("issue");
export function
function issueQuery(input: {
    id: number;
}): QueryOptions<readonly ["issue", number], any, any, DefaultTypes>
issueQuery
(
input: {
    id: number;
}
input
: { id: numberid: number }) {
return
queryOptions<readonly ["issue", number], any, any>(options: BaseOptions<readonly ["issue", number], DefaultTypes> & {
    readonly queryFn: (context: QueryFunctionContext<readonly ["issue", number]>) => any;
    readonly select?: ((data: any) => any) | undefined;
}): QueryOptions<readonly ["issue", number], any, any, DefaultTypes> (+1 overload)

Typed options for a query factory:

queryOptions
({
BaseOptions<readonly ["issue", number], DefaultTypes>.queryKey: readonly ["issue", number]

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

queryKey
:
const issue: Group
<readonly [number]>(rest_0: number) => readonly ["issue", number]
issue
(
input: {
    id: number;
}
input
.id: numberid),
queryFn: (context: QueryFunctionContext<readonly ["issue", number]>) => anyqueryFn: (context: QueryFunctionContext<readonly ["issue", number]>context) => function apiFetch(path: string, init?: RequestInit): Promise<Response>apiFetch(`/api/issues/${
input: {
    id: number;
}
input
.id: numberid}`, { RequestInit.signal?: AbortSignal | null | undefined

An AbortSignal to set request's signal.

signal
: context: QueryFunctionContext<readonly ["issue", number]>context.QueryFunctionContext<readonly ["issue", number]>.signal: AbortSignal

Aborted when the request is restarted or its entry is removed.

signal
}).Promise<Response>.then<any, never>(onfulfilled?: ((value: Response) => any) | null | undefined, onrejected?: ((reason: any) => PromiseLike<never>) | null | undefined): Promise<any>

Attaches callbacks for the resolution and/or rejection of the Promise.

@paramonfulfilled — The callback to execute when the Promise is resolved.@paramonrejected — The callback to execute when the Promise is rejected.@returnsA Promise for the completion of which ever callback is executed.
then
(
(response: Responseresponse) => response: Responseresponse.Body.json(): Promise<any>json(), ), }); }
  • The API sees the same request from both sides, so it authenticates the same way. Forward only what it reads (cookie, authorization), not every header.
  • Nothing needs the app's own origin, and server rendering doesn't open a second connection to itself.
  • A typed client works the same way. examples/nextjs gives a Hono client the page request's cookies in src/forwarded-headers.server.ts.
  • Reading headers() renders the page per request, as a page with per-user data must.

An API on another origin

The server can call the same URL as the browser, but the browser's cookies belong to your origin, not the API's:

  • In server components, forward the credentials it needs: read your session with cookies() and send, for example, an Authorization header.
  • In the browser, either proxy it through a Route Handler, so the request carries your cookies and the handler adds the same credentials, or send credentials: "include" to an API that allows your origin with CORS and sets its own cookies.

Pick the two implementations with the same react-server condition.

Server actions

Factories can call server actions, and get both sides for free: server components call the function in the page request, and the browser posts to it with its cookies. Validate their arguments, since anyone can call them.

The browser runs a page's server actions one at a time, so refetches wait for each other and for any pending mutation. For parallel requests, call your API instead.

Mutations

Write with a server action, then invalidate in the browser:

src/components/update-issue-form.tsx
return (
	<JSX.IntrinsicElements.form: DetailedHTMLProps<FormHTMLAttributes<HTMLFormElement>, HTMLFormElement>form
		FormHTMLAttributes<HTMLFormElement>.action?: string | ((formData: FormData) => void | Promise<void>) | undefinedaction={async (formData: FormDataformData) => {
			await function updateIssue(_formData: FormData): Promise<void>updateIssue(formData: FormDataformData);
			await 
invalidateOne<(input: {
    id: number;
}) => QueryOptions<readonly ["issue", number], any, any, DefaultTypes>>(client: ClientFor<(input: {
    id: number;
}) => QueryOptions<readonly ["issue", number], any, any, DefaultTypes>>, factory: (input: {
    id: number;
}) => QueryOptions<readonly ["issue", number], any, any, DefaultTypes>, input: {
    id: number;
}): Promise<void>

Mark one query stale: a factory and its full input. If it's read, it refetches; resolves when that settles.

invalidateOne
(const qc: QueryClient<any>qc,
function issueQuery(input: {
    id: number;
}): QueryOptions<readonly ["issue", number], any, any, DefaultTypes>
issueQuery
, { id: numberid:
props: {
    id: number;
}
props
.id: numberid });
}} /> );

invalidateOne resolves when the refetch of the query on screen settles, so the form stays pending until the new data shows. When the write changes more than one thing, pass a list to one invalidateMany call, so the refetches run side by side. See Mutations.

If the action also calls revalidatePath or refresh(), server components render again and send their entries, and the browser keeps whichever copy is newest.

Server-only queries

A factory that imports server-only code (a database client) can't be imported by a client component, so it can't be read with useQuery. Render its data in a server component, or pass a plain promise prop and use() it on the client. Refresh it with router.refresh().

To read it from client components too, put the server-only code behind an API, and reach that from the factory as in An API in the same app.

Example

examples/nextjs is the same issue tracker as the TanStack Start example, built on the Next.js App Router, so the two integrations can be compared side by side.