# TanStack Start

> Set up raf-query with TanStack Router and Start, start queries in loaders, stream them during SSR and handle redirects.

TanStack Router's loaders are where raf-query wants queries to start, and its SSR streaming is what carries the cache to the browser. The integration is one function call.

## Setup

```ts title="src/query-client.ts"
import { createQueryClient, recommended } from "@raf-query/react";

export function makeQueryClient() {
	return createQueryClient({ ...recommended(), staleTime: 30_000 });
}
```

```tsx title="src/router.tsx"
import { QueryDevtools } from "@raf-query/react";
import { setupRouterIntegration } from "@raf-query/react/tanstack-router";
import { createRouter } from "@tanstack/react-router";

import { Providers } from "./providers.tsx";
import { makeQueryClient } from "./query-client.ts";
import { routeTree } from "./routeTree.gen.ts";

export function getRouter() {
	const queryClient = makeQueryClient();
	const router = createRouter({
		context: { queryClient },
		defaultPreload: "intent",
		// The query cache decides what's fresh; loaders always run and dedupe there.
		defaultPreloadStaleTime: 0,
		routeTree,
		scrollRestoration: true,
		Wrap: (props) => (
			<Providers client={queryClient}>
				{props.children}
				{import.meta.env.DEV ? <QueryDevtools /> : null}
			</Providers>
		),
	});
	setupRouterIntegration({ queryClient, router });
	return router;
}
```

`makeQueryClient` is a function, not a client at module level: a module-level client on the server would share one cache between every request. `Providers` is the [provider component every framework shares](https://raf-query.katt.dev/docs/ssr-and-hydration.md#the-provider); here it gets the router's client, the one loaders use:

```tsx title="src/providers.tsx"
"use client";

import { type QueryClient, QueryClientProvider } from "@raf-query/react";
import { type ReactNode, useState } from "react";

import { 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 Providers(props: {
	readonly children: ReactNode;
	readonly client?: QueryClient;
}) {
	const [client] = useState(() => props.client ?? makeQueryClient());
	return (
		<QueryClientProvider client={client}>{props.children}</QueryClientProvider>
	);
}
```

Mount DevTools only when `import.meta.env.DEV` is true so Vite 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.

Type the router context in the root route, so loaders see `queryClient`:

```tsx title="src/routes/__root.tsx"
import type { QueryClient } from "@raf-query/react";
import { createRootRouteWithContext } from "@tanstack/react-router";

export const Route = createRootRouteWithContext<{ queryClient: QueryClient }>()(
	{},
);
```

`getRouter` runs once per request on the server and once in the browser, so each request gets its own client and the browser keeps one for the session.

## What the integration does

`setupRouterIntegration`:

- **Streams queries during SSR.** Entries go to the browser as they settle, through the router's own stream. Pending queries cross as promises.
- **Clears the request's cache** when the response finishes.
- **Turns `redirect()` thrown from a `queryFn` into a navigation**, wherever the query failed: a loader, a render or a background refetch in the browser.

Options:

| Option             | Use                                                                                                      |
| ------------------ | -------------------------------------------------------------------------------------------------------- |
| `queryClient`      | The router's client                                                                                      |
| `router`           | The router                                                                                               |
| `dehydrateOptions` | `filter` which entries are sent; `keepErrors` to send a server request's error instead of a redacted one |
| `handleRedirects`  | `false` to handle redirects yourself                                                                     |

A query whose server request throws reaches the browser as a `RedactedError`, which names the query but not the cause, so server errors don't leak into the page.

A component that reads a query no loader started, such as a [`waterfallHandle`](https://raf-query.katt.dev/docs/request-waterfalls.md#render-time-inputs-waterfallhandle)'s, fetches it on the server while rendering. The integration streams that entry too, so the HTML has the data and the browser doesn't fetch it again. It's still a waterfall: the request starts only once the component renders. See [SSR and hydration](https://raf-query.katt.dev/docs/ssr-and-hydration.md#queries-nothing-started-on-the-server).

## Loaders

A loader starts every query its route needs with `load` and returns the handles:

```tsx title="src/routes/posts.$slug.tsx"
import {
	invalidateMany,
	load,
	prefetch,
	type QueryHandle,
	swr,
	useInfiniteQuery,
	useQuery,
	useQueryClient,
} from "@raf-query/react";
import { createFileRoute } from "@tanstack/react-router";
import { Suspense, use } from "react";

import { addComment } from "~/api.ts";
import { CommentsSkeleton, renderComment } from "~/components/comments.tsx";
import { commentsQuery, postQuery } from "~/queries.ts";

export const Route = createFileRoute("/posts/$slug")({
	loader: async (args) => {
		const { slug } = args.params;
		const [handles] = await load(args.context.queryClient, {
			// Waits only if the post isn't in the cache yet.
			post: swr(postQuery, { slug }),
			// Never waits: the comments load while the page renders.
			comments: prefetch(commentsQuery, { slug }),
		});
		return handles;
	},
	component: PostPage,
});

function PostPage() {
	const data = Route.useLoaderData();
	// The route already waited for this, so this is the post itself.
	const [postThenable] = useQuery(postQuery, data.post);
	const post = use(postThenable);
	// const post: Post
	return (
		<article>
			<h1>{post.title}</h1>
			<Suspense fallback={<CommentsSkeleton />}>
				<Comments handle={data.comments} />
			</Suspense>
			<AddComment slug={post.slug} />
		</article>
	);
}

function Comments(props: { handle: QueryHandle<typeof commentsQuery> }) {
	// May still be loading. If so, use() suspends until it lands.
	const [pagesThenable, commentsState] = useInfiniteQuery(
		commentsQuery,
		props.handle,
	);
	const pages = use(pagesThenable);
	// const pages: InfiniteData<CommentPage, number>
	return (
		<>
			{pages.pages.flatMap((page) => page.items).map(renderComment)}
			{commentsState.hasNextPage ? (
				<button
					disabled={commentsState.isFetchingNextPage}
					onClick={() => commentsState.fetchNextPage()}
				>
					{commentsState.isFetchingNextPage ? "Loading…" : "Load more"}
				</button>
			) : null}
		</>
	);
}

function AddComment(props: { slug: string }) {
	const queryClient = useQueryClient();
	return (
		<form
			action={async (formData) => {
				await addComment({
					body: String(formData.get("body")),
					slug: props.slug,
				});
				await invalidateMany(queryClient, [
					[commentsQuery, { slug: props.slug }],
				]);
			}}
		>
			<textarea name="body" required />
			<button>Add comment</button>
		</form>
	);
}
```

See [Starting queries](https://raf-query.katt.dev/docs/starting-queries.md) for choosing between `prefetch`, `swr` and `revalidate`.

### Handles in route context

`beforeLoad` can return handles too. They're part of the route context, so child routes' loaders and components read them, and the router serializes them to the browser with the rest of the context:

```tsx title="src/routes/_auth.tsx"
import { load, swr } from "@raf-query/react";
import { createFileRoute, redirect } from "@tanstack/react-router";

import { meQuery } from "~/queries.ts";

export const Route = createFileRoute("/_auth")({
	beforeLoad: async (args) => {
		const [handles, data] = await load(args.context.queryClient, {
			me: swr(meQuery),
		});
		if (data.me === null) {
			throw redirect({ to: "/login" });
		}
		return { me: handles.me };
	},
});
```

```tsx title="src/routes/_auth.dashboard.tsx"
function Dashboard() {
	const context = Route.useRouteContext();
	const [meThenable] = useQuery(meQuery, context.me);
	const me = use(meThenable);
	return <p>{me?.name}</p>;
}
```

Importing `@raf-query/react/tanstack-router` registers handles as serializable, so Start's type check of `beforeLoad`'s return accepts them.

### Router settings

- **`defaultPreload: "intent"`** runs a route's loader when the user hovers or focuses a link, so the queries start before the click.
- **`defaultPreloadStaleTime: 0`** makes the router run the loader again on click. Otherwise it reuses a hover's preload for 30 seconds, and the query cache never hears about the navigation. The query cache dedupes, so running the loader again is free when the data is fresh.

### Pending UI

While a loader's `swr` entry is still loading, the previous page stays on screen. Show that a navigation is in progress with the router — not [`useIsFetching`](https://raf-query.katt.dev/docs/background-fetching-indicators.md#useisfetching):

- **`pendingComponent` and `pendingMs`** show a full-page skeleton after the delay (1 second by default) for routes whose loader waits for slow data.
- **`useRouterState({ select: (state) => state.isLoading })`** drives a progress bar or spinner in the layout for any pending navigation:

```tsx
function NavigationProgress() {
	const isLoading = useRouterState({ select: (state) => state.isLoading });
	return isLoading ? <ProgressBar /> : null;
}
```

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

### Search params

Use `loaderDeps` to pass validated search params to the loader, so it reruns when they change. See [URL state and pagination](https://raf-query.katt.dev/docs/url-state-and-pagination.md).

## Server and browser requests

A loader runs on the server during SSR and in the browser after, so a factory's `queryFn` runs in both.

### Server functions

Factories can call server functions (`createServerFn`) directly, and get both sides for free. During SSR the call is a function call that sees the page request; in the browser, refetches call the same function over HTTP, with the browser's cookies.

```ts title="src/queries.ts"
export function postQuery(input: { slug: string }) {
	return queryOptions({
		queryKey: posts("bySlug", input.slug),
		queryFn: () => getPost({ data: input }),
	});
}
```

### Your own API

A factory that calls an API needs the user's cookies on both sides. The browser sends them; during SSR, forward the page request's:

```ts title="src/forwarded-headers.ts"
import { createIsomorphicFn } from "@tanstack/react-start";
import { getRequestHeader } from "@tanstack/react-start/server";

export const forwardedHeaders = createIsomorphicFn()
	.server((): Record<string, string> => {
		const cookie = getRequestHeader("cookie");
		return cookie === undefined ? {} : { cookie };
	})
	.client((): Record<string, string> => ({}));
```

- Send `forwardedHeaders()` with each request: Hono's `hc` and tRPC's and oRPC's links take a `headers` function.
- Forward only what the API reads (`cookie`, `authorization`), not every header.
- For an API in the same app, call it in the process during SSR instead of over HTTP to your own origin. [`examples/tanstack-start`](https://github.com/KATT/raf-query/tree/main/examples/tanstack-start) picks an in-process client on the server and an HTTP one in the browser with a `browser` export condition, so the API's code stays out of the browser bundle.

## Static prerendering

These docs are a TanStack Start app, prerendered to static HTML. Every page's loader waits for its content with `swr` during the build, the content crosses in the page's HTML, and later navigations load it in the browser. See the [source](https://github.com/KATT/raf-query/tree/main/www).

## Example

[`examples/tanstack-start`](https://github.com/KATT/raf-query/tree/main/examples/tanstack-start) is a complete issue tracker: lists with filters and infinite scroll, details with comments, and mutations with optimistic updates.
