# Quick start

> Create a query client, define a post and its paged comments, start them in a TanStack Start loader, read them with useQuery, useInfiniteQuery and use(), and add a comment.

This walks through the three core concepts of raf-query on [TanStack Start](https://tanstack.com/start):

- **Query factories**, which describe a query once
- **Starting queries** in a loader, which hands components a handle
- **Reading queries** with `useQuery` and `use()`

For the Next.js App Router, the same steps apply with a server component in place of the loader: see [React Server Components](https://raf-query.katt.dev/docs/react-server-components.md). For React Router Framework Mode, see [React Router](https://raf-query.katt.dev/docs/react-router.md).

## 1. Create the client

Create one client per router with `createQueryClient`. On the server, `getRouter` runs once per request, so each request gets its own cache; in the browser it runs once per tab.

```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;
}
```

`Providers` hands the router's client to the components below it with `QueryClientProvider`. The same component works in [every framework](https://raf-query.katt.dev/docs/ssr-and-hydration.md#the-provider):

```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>
	);
}
```

The client does nothing on its own. `recommended()` turns on what most apps want: retries, refetching stale data on window focus and reconnect, polling, and refetching after [actions](https://raf-query.katt.dev/docs/mutations.md). `staleTime` keeps data fresh for 30 seconds, so navigating back to a page doesn't refetch it.

`setupRouterIntegration` streams the server's cache to the browser while the page renders, so data loaded on the server is never fetched twice. In development, [DevTools](https://raf-query.katt.dev/docs/devtools.md) show what's in the cache. Give the root route the client's type:

```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 }>()(
	{},
);
```

## 2. Define the queries

The page is a blog post with its comments. A query is a **factory**: a function of at most one argument that returns `queryOptions(...)`, or `infiniteQueryOptions(...)` for a list that loads page by page, like the comments. The argument is the query's input, here the post's `slug`. Loaders and components call the same factory, so the data's type flows end to end.

```ts title="src/queries.ts"
import { group, infiniteQueryOptions, queryOptions } from "@raf-query/react";

import { type CommentPage, listComments } from "./api.ts";
import { getPost } from "./posts.functions.ts";

export const posts = group("posts");
// A post's comments are a group inside `posts`.
export const comments = group(posts, "comments");

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

export function commentsQuery(input: { slug: string }) {
	return infiniteQueryOptions({
		queryKey: comments(input.slug),
		queryFn: (context) =>
			listComments({ cursor: context.pageParam, slug: input.slug }),
		initialPageParam: 0,
		getNextPageParam: (lastPage: CommentPage) => lastPage.nextCursor,
	});
}
```

The keys come from **groups**. `comments` is a group inside `posts`, so one call can [invalidate](https://raf-query.katt.dev/docs/query-invalidation.md) every post query and every comment list at once, or only the comments.

`listComments` returns a page of comments and the cursor of the next one, `null` after the last page. `getNextPageParam` hands that cursor to the next request, as in [TanStack Query](https://tanstack.com/query/latest/docs/framework/react/guides/infinite-queries).

## 3. Start them in the loader

The loader starts both queries in one `load` and returns their **handles**, small `{ queryKey, input }` objects:

```tsx title="src/routes/posts.$slug.tsx"
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,
});
```

`swr` waits for the post when the cache doesn't have it yet, because the page can't lay out without it. `prefetch` starts the comments without waiting, so the page renders while they're on their way. [Starting queries](https://raf-query.katt.dev/docs/starting-queries.md) covers the trade-off.

## 4. Read them in components

```tsx title="src/routes/posts.$slug.tsx"
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}
		</>
	);
}
```

`useQuery` takes the factory as well as the handle because the handle is plain data, `{ queryKey, input }`, so it can cross from a loader or a server component to the browser, and a function can't: the handle can't carry the `queryFn`, so you pass the factory alongside it.

`useQuery` returns a stable thenable and the query's state. `use()` suspends until the data is there, then returns it. The loader already waited for the post, so the thenable is fulfilled and `use()` returns it without suspending.

The comments may still be loading, so `<Comments>` sits in its own Suspense boundary and the post renders without them. `useInfiniteQuery` reads them as `{ pages, pageParams }`, and its state loads more: `fetchNextPage()` fetches the next page and appends it, `hasNextPage` says whether there is one, and `isFetchingNextPage` is `true` while it loads. See [Infinite queries](https://raf-query.katt.dev/docs/infinite-queries.md).

## 5. Write data with an Action

There is no `useMutation`. Adding a comment is a form action in the component: await the write, then invalidate what it changed with the client from `useQueryClient()`:

```tsx title="src/routes/posts.$slug.tsx"
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>
	);
}
```

`invalidateMany` takes a list of targets. `[commentsQuery, { slug }]` is this post's comments, and the list refetches every page it has loaded. The verb resolves once that refetch has settled, so the form stays pending until the new comment is on screen. See [Mutations](https://raf-query.katt.dev/docs/mutations.md) for optimistic updates and errors.

## What you get

The cache always dedupes requests, garbage-collects unused entries and keeps unchanged data referentially stable. With `recommended()`, it also retries failures and refetches stale data when the window regains focus. Read [Important defaults](https://raf-query.katt.dev/docs/important-defaults.md) next: they explain when raf-query fetches and when it doesn't.
