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():
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(): RecommendedThe 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(): RecommendedThe behaviors most apps want: retries, refetching on focus and reconnect,
and polling. Spread it into the config:
recommended(), QueryClientConfig.staleTime?: number | undefinedDefault 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:
"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 | undefinedRepresents 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 | undefinedRepresents 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:
import { function QueryDevtools(props: QueryDevtoolsProps): JSX.ElementA 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 | undefinedRepresents 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 | undefinedRepresents 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.ProcessEnvThe process.env property returns an object containing the user environment.
See environ(7).
env.string | undefinedNODE_ENV !== "production" ? <function QueryDevtools(props: QueryDevtoolsProps): JSX.ElementA 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:
import { function HydrationBoundary(props: HydrationBoundaryProps): JSX.ElementWrites 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 { const Suspense: ExoticComponent<SuspenseProps>Lets you display a fallback until its children have finished loading.
Suspense } from "react";
import { function Comments(props: {
handle: QueryHandle<typeof commentsQuery>;
}): JSX.Element[]
Comments } from "../../components/comments.tsx";
import { function CommentsSkeleton(): JSX.ElementCommentsSkeleton } from "../../components/comments-skeleton.tsx";
import { function PostSkeleton(): JSX.ElementPostSkeleton } from "../../components/post-skeleton.tsx";
import { function Post(props: {
handle: QueryHandle<typeof postQuery>;
}): JSX.Element
Post } from "../../components/post.tsx";
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.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} />);
}
- Render a
HydrationBoundaryin each server component that starts queries for client components. Entries already sent in this request are skipped, so nesting them is fine. clientworks only in server components. The bundler resolves@raf-query/reactwith thereact-serverexport condition there, which picks theHydrationBoundarythat dehydrates the client. Client components get the one that takes a dehydratedstate, as in React Router.- Pending entries stream as promises through the RSC payload: the page's shell renders while
commentsloads. optionstakesdehydrate's options, such askeepErrorsandfilter.- Use
prefetchandrevalidatein server components, notswr. Each request's client starts empty, soswrwaits for the request every time, exactly likerevalidate. 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.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.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?: 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>
</function HydrationBoundary(props: HydrationBoundaryProps): JSX.ElementWrites 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.
"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); 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:
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 components | Browser | |
|---|---|---|
| When | The page request | Refetches and later pages |
fetch("/api/...") | Fails: there is no page origin to resolve it | Works |
| The user's cookies | Yours 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:
{
"imports": {
"#api-fetch": {
"react-server": "./src/api-fetch.server.ts",
"default": "./src/api-fetch.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) => HeadersThe 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 | undefinedA 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 | nullThe 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): voidThe 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) => URLThe 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) => RequestThe Request interface of the Fetch API represents a resource request.
Request(const url: URLurl, { ...init: RequestInit | undefinedinit, RequestInit.headers?: HeadersInit | undefinedA Headers object, an object literal, or an array of two-item arrays to set request's headers.
headers: const forwarded: Headersforwarded }));
}// 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);
}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 | undefinedAn AbortSignal to set request's signal.
signal: context: QueryFunctionContext<readonly ["issue", number]>context.QueryFunctionContext<readonly ["issue", number]>.signal: AbortSignalAborted 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.
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/nextjsgives a Hono client the page request's cookies insrc/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, anAuthorizationheader. - 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:
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.