# 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](https://react.dev/reference/rsc/server-components), server components play the part of loaders: they start queries and pass handles to client components. `QueryProvider` and `HydrationBoundary`, both from `@raf-query/react`, connect them.

The samples use the [Next.js App Router](https://nextjs.org/docs/app). 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](https://github.com/vitejs/vite-plugin-react/tree/main/packages/plugin-rsc), 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

A query client per request for server components, scoped by React's [`cache()`](https://react.dev/reference/react/cache):

```ts title="src/query-client.ts"
import { QueryClient, type QueryClientConfig } from "@raf-query/react";
import { cache } from "react";

export const queryClientConfig = {
	defaults: { staleTime: 5000 },
} as const satisfies QueryClientConfig;

export const getQueryClient = cache(() => new QueryClient(queryClientConfig));
```

`QueryProvider` in the root layout, for client components:

```tsx title="src/app/layout.tsx"
import { QueryProvider } from "@raf-query/react";
import { QueryDevtools } from "@raf-query/react/devtools";
import type { ReactNode } from "react";

import { queryClientConfig } from "~/query-client.ts";

export default function RootLayout(props: { children: ReactNode }) {
	return (
		<html lang="en">
			<body>
				<QueryProvider config={queryClientConfig}>
					{props.children}
					{process.env.NODE_ENV !== "production" ? <QueryDevtools /> : null}
				</QueryProvider>
			</body>
		</html>
	);
}
```

`QueryProvider` creates one client per server render and one per browser tab. Its `config` must be serializable, since it crosses from the server component layout to the client.

Mount DevTools only when `process.env.NODE_ENV !== "production"` so the bundler drops the panel from production builds. The [example app](https://raf-query.katt.dev/docs/examples.md) always renders it, so the panel stays available in the deployed demo.

## Pages

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

```tsx title="src/app/(blog)/posts/[slug]/page.tsx"
import { HydrationBoundary } from "@raf-query/react";
import { Suspense } from "react";

import { Comments } from "../../components/comments.tsx";
import { CommentsSkeleton } from "../../components/comments-skeleton.tsx";
import { PostSkeleton } from "../../components/post-skeleton.tsx";
import { Post } from "../../components/post.tsx";
import { commentsQuery, postQuery } from "~/queries.ts";
import { getQueryClient } from "~/query-client.ts";

export default async function Page(props: {
	params: Promise<{ slug: string }>;
}) {
	const { slug } = await props.params;
	const qc = getQueryClient();
	// Never waits: both load while the page streams.
	const comments = qc.prefetch(commentsQuery, { slug });
	const post = qc.prefetch(postQuery, { slug });
	return (
		<HydrationBoundary client={qc}>
			<article>
				<Suspense fallback={<PostSkeleton />}>
					<Post handle={post} />
				</Suspense>
				<Suspense fallback={<CommentsSkeleton />}>
					<Comments handle={comments} />
				</Suspense>
			</article>
		</HydrationBoundary>
	);
}
```

```tsx title="src/app/(blog)/components/post.tsx"
"use client";
export function Post(props: { handle: QueryHandle<typeof postQuery> }) {
	const [postPromise] = useQuery(postQuery, props.handle);
	// const postPromise: Thenable<Post>
	const post = use(postPromise);
	return <h1>{post.title}</h1>;
}
```

```tsx title="src/app/(blog)/components/comments.tsx"
"use client";
export function Comments(props: { handle: QueryHandle<typeof commentsQuery> }) {
	// May still be loading. If so, use() suspends until it lands.
	const [pagesPromise] = useQuery(commentsQuery, props.handle);
	const pages = use(pagesPromise);
	// const pages: InfiniteData<{
	//     page: CommentPage;
	//     pageParam: number;
	// }>
	return pages.pages
		.flatMap((page) => page.items)
		.map((comment) => <Comment key={comment.id} comment={comment} />);
}
```

- 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](https://nodejs.org/api/packages.html#conditional-exports) there, which picks the `HydrationBoundary` that dehydrates the client. Client components get the one that takes a dehydrated `state`, as in [React Router](https://raf-query.katt.dev/docs/react-router.md).
- Pending entries stream as promises through the RSC payload: the page's shell renders while `comments` loads.
- `await qc.fetch(...)` returns a handle with the `data`: render `post.data` in the server component, and pass `post` to client components that read the query: typed `QueryHandleWithData`, they get the data from `useQuery` without a promise. The data crosses once: the RSC payload references the `HydrationBoundary`'s copy.
- Use `prefetch` and `fetch` in server components, not `ensure`. Each request's client starts empty, so `ensure` waits for the request every time, exactly like `fetch`. See [Starting queries](https://raf-query.katt.dev/docs/starting-queries.md#server-only-code).

## Pending UI

While a link navigation is loading the next RSC payload, show pending state with the framework's router — not [`useIsFetching`](https://raf-query.katt.dev/docs/background-fetching-indicators.md#useisfetching). In Next.js, render [`useLinkStatus`](https://nextjs.org/docs/app/api-reference/functions/use-link-status) as a child of the `Link` it tracks:

```tsx
"use client";

function PendingHint() {
	const { pending } = useLinkStatus();
	return pending ? <Spinner /> : null;
}

<Link href="/issues">
	Issues
	<PendingHint />
</Link>;
```

Per-query background refetches after the page has rendered use `controls.isFetching`. See [Background fetching indicators](https://raf-query.katt.dev/docs/background-fetching-indicators.md).

## Search params

Read [`searchParams`](https://nextjs.org/docs/app/api-reference/file-conventions/page#searchparams-optional) in the page, validate them, and pass them as the input:

```tsx title="src/app/issues/page.tsx"
export default async function IssuesPage(props: {
	searchParams: Promise<Record<string, string | Array<string>>>;
}) {
	const input = IssueListInput.parse(await props.searchParams);
	const qc = getQueryClient();
	const issues = qc.prefetch(issueListQuery, input);
	// ...
```

Changing search params with [`<Link>`](https://nextjs.org/docs/app/api-reference/components/link) or [`router.push`](https://nextjs.org/docs/app/api-reference/functions/use-router) renders the page again on the server with the new input. See [URL state and pagination](https://raf-query.katt.dev/docs/url-state-and-pagination.md) for what to show while it loads.

## Queries that start in the browser

`QueryProvider`'s server render starts no requests. 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 with [`useWaterfall`](https://raf-query.katt.dev/docs/request-waterfalls.md#render-time-inputs-usewaterfall) in a client component. 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.

## 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()`](https://nextjs.org/docs/app/api-reference/functions/headers) | Sent by the browser       |

Client components' server render starts no request, so it isn't a third place.

### 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](https://nodejs.org/api/packages.html#subpath-imports) 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:

```json title="package.json"
{
	"imports": {
		"#api-fetch": {
			"react-server": "./src/api-fetch.server.ts",
			"default": "./src/api-fetch.ts"
		}
	}
}
```

```ts title="src/api-fetch.server.ts"
import { headers } from "next/headers";

// The function `app/api/[...path]/route.ts` exports as `GET` and `POST`.
import { handler } from "./api-handler.ts";

export async function apiFetch(path: string, init?: RequestInit) {
	const forwarded = new Headers(init?.headers);
	const cookie = (await headers()).get("cookie");
	if (cookie !== null) {
		forwarded.set("cookie", cookie);
	}
	// The host is a placeholder: the handler routes by path.
	const url = new URL(path, "http://localhost");
	return handler(new Request(url, { ...init, headers: forwarded }));
}
```

```ts title="src/api-fetch.ts"
// The browser sends the page's cookies to its own origin.
export function apiFetch(path: string, init?: RequestInit) {
	return fetch(path, init);
}
```

```ts title="src/queries.ts"
import { apiFetch } from "#api-fetch";

export function issueQuery(input: { id: number }) {
	return queryOptions({
		queryKey: ["issue", input.id],
		queryFn: (context) =>
			apiFetch(`/api/issues/${input.id}`, { signal: context.signal }).then(
				(response) => response.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`](https://github.com/KATT/raf-query/tree/main/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()`](https://nextjs.org/docs/app/api-reference/functions/cookies) and send, for example, an `Authorization` header.
- **In the browser**, either proxy it through a [Route Handler](https://nextjs.org/docs/app/api-reference/file-conventions/route), so the request carries your cookies and the handler adds the same credentials, or send [`credentials: "include"`](https://developer.mozilla.org/en-US/docs/Web/API/RequestInit#credentials) to an API that allows your origin with [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS) and sets its own cookies.

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

### Server actions

Factories can call [server actions](https://react.dev/reference/rsc/server-functions), 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:

```tsx title="src/components/update-issue-form.tsx"
return (
	<form
		action={async (formData) => {
			await updateIssue(formData);
			await qc.invalidate(issueQuery, { id: props.id });
		}}
	/>
);
```

If the action also calls [`revalidatePath`](https://nextjs.org/docs/app/api-reference/functions/revalidatePath) or [`refresh()`](https://nextjs.org/docs/app/api-reference/functions/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()`](https://nextjs.org/docs/app/api-reference/functions/use-router).

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](#an-api-in-the-same-app).

## Example

[`examples/nextjs`](https://github.com/KATT/raf-query/tree/main/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.
