# TanStack Query and SWR with Next.js [#tanstack-query-and-swr-with-nextjs]
In a [Next.js app that hosts Aventara](/framework/docs/nextjs-host), a Server Component can fetch the data a Client Component needs, and the Client Component picks it up without a second request. This page shows it with TanStack Query (v5) and with SWR.
Three pieces make it work:
| Piece | From | What it does |
| ------------- | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `avClient` | `@/aventara/universal` | The same client on both sides: in process on the server, `fetch` to `/api` in the browser. Same types, same values. |
| `avDehydrate` | `@aventara/next` | On the server: turns each `Date`, `bigint`, `Uint8Array` and decimal into a tagged plain object that React can pass to the browser. |
| `avHydrate` | `@/aventara/client` | In the browser: turns those objects back into `Date`, `bigint`, `Uint8Array` and the client's `AvDecimal`. |
`avDehydrate` is needed because React refuses to pass a class instance from a Server Component to a Client Component, and a decimal is an `AvDecimal` instance. A result with no decimal passes without it, but using the pair everywhere keeps one rule.
The universal client needs `src/instrumentation.ts` to await `ready()`, as the scaffold writes it ([Startup](/framework/docs/nextjs-host#startup-instrumentationts)).
## TanStack Query [#tanstack-query]
```bash
npm install @tanstack/react-query
```
### 1. One query module for both sides [#1-one-query-module-for-both-sides]
```ts
// src/app/posts/posts.queries.ts
import { queryOptions } from "@tanstack/react-query";
import { avClient } from "@/aventara/universal";
export const postsQuery = queryOptions({
queryKey: ["posts"],
queryFn: () =>
avClient.Post.find.many({
where: { published: true },
select: ["id", "title", "publishedAt", "price"],
}),
staleTime: 60_000,
});
```
The Server Component prefetches with it and the Client Component reads with it, so the key and the function are written once.
Import `@/aventara/universal` here, not the others. `@/aventara/server` imports `server-only`, so a Client Component that imports this module would fail `next build`. `@/aventara/client` refuses calls on the server, so the prefetch would fail.
### 2. The provider, with `avHydrate` [#2-the-provider-with-avhydrate]
```tsx
// src/app/providers.tsx
"use client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { type ReactNode, useState } from "react";
import { avHydrate } from "@/aventara/client";
export function Providers({ children }: { readonly children: ReactNode }) {
const [queryClient] = useState(
() => new QueryClient({ defaultOptions: { hydrate: { deserializeData: avHydrate } } }),
);
return {children};
}
```
```tsx
// src/app/layout.tsx
import type { ReactNode } from "react";
import { Providers } from "./providers";
export default function RootLayout({ children }: { readonly children: ReactNode }) {
return (
{children}
);
}
```
`defaultOptions.hydrate.deserializeData` runs on every query the client hydrates. To set it on one boundary instead, pass it to `HydrationBoundary`: `options={{ defaultOptions: { deserializeData: avHydrate } }}`. Outside React, `hydrate(queryClient, state, { defaultOptions: { deserializeData: avHydrate } })` does the same.
### 3. Prefetch in a Server Component, dehydrate with `avDehydrate` [#3-prefetch-in-a-server-component-dehydrate-with-avdehydrate]
```tsx
// src/app/posts/page.tsx
import { avDehydrate } from "@aventara/next";
import { dehydrate, HydrationBoundary, QueryClient } from "@tanstack/react-query";
import { Suspense } from "react";
import { PostList } from "./post-list";
import { postsQuery } from "./posts.queries";
async function Prefetched() {
const queryClient = new QueryClient();
await queryClient.prefetchQuery(postsQuery);
return (
);
}
export default function Page() {
return (
Loading}>
);
}
```
A new `QueryClient` per request keeps one user's data out of another's. The `` is what `cacheComponents` (create-next-app's default) asks of a page that reads per-request data: every universal call on the server first awaits Next's `io()`, so it runs at request time ([Next.js host](/framework/docs/nextjs-host#server-components-and-server-actions)). The universal client reads the request's headers for `state`, so the data is per request.
### 4. Read it with `useQuery` [#4-read-it-with-usequery]
```tsx
// src/app/posts/post-list.tsx
"use client";
import { useQuery } from "@tanstack/react-query";
import { postsQuery } from "./posts.queries";
export function PostList() {
const { data } = useQuery(postsQuery);
return (
{data?.map((post) => (
-
{post.title}: {post.publishedAt?.toLocaleDateString()} {post.price?.toString()}
))}
);
}
```
The first render uses the prefetched data: `publishedAt` is a `Date` and `price` an `AvDecimal`, as they would be from a browser fetch. A refetch (after `staleTime`, on focus) calls `/api` from the browser and gets the same types.
## A helper that always picks the universal client [#a-helper-that-always-picks-the-universal-client]
If many modules define queries, a small helper keeps them all on `@/aventara/universal`:
```ts
// src/lib/av-query.ts
import { queryOptions } from "@tanstack/react-query";
import { avClient } from "@/aventara/universal";
type Av = typeof avClient;
export function avQuery(queryKey: readonly unknown[], read: (av: Av) => Promise) {
return queryOptions({ queryKey, queryFn: () => read(avClient) });
}
```
```ts
// src/app/posts/posts.queries.ts
import { avQuery } from "@/lib/av-query";
export const draftsQuery = avQuery(["posts", "drafts"], (av) =>
av.Post.find.many({ where: { published: false }, select: ["id", "title"] }),
);
```
The result type follows the `select`, as with a direct call. The modules that define queries never import a client themselves, so none of them can pick `@/aventara/server` or `@/aventara/client` by mistake.
## SWR [#swr]
```bash
npm install swr
```
Read on the server with the universal client, and pass the result as SWR's `fallback`, dehydrated:
```tsx
// src/app/drafts/page.tsx
import { avDehydrate } from "@aventara/next";
import { Suspense } from "react";
import { avClient } from "@/aventara/universal";
import { Drafts } from "./drafts";
async function Prefetched() {
const drafts = await avClient.Post.find.many({
where: { published: false },
select: ["id", "title", "publishedAt"],
});
return ;
}
export default function Page() {
return (
Loading}>
);
}
```
```tsx
// src/app/drafts/drafts.tsx
"use client";
import useSWR, { SWRConfig } from "swr";
import { avClient, avHydrate } from "@/aventara/client";
function DraftList() {
const { data } = useSWR("drafts", () =>
avClient.Post.find.many({
where: { published: false },
select: ["id", "title", "publishedAt"],
}),
);
return {data?.map((post) => - {post.title}
)}
;
}
export function Drafts({ fallback }: { readonly fallback: unknown }) {
return (
}}>
);
}
```
The `fallback` key (`"drafts"`) is the `useSWR` key. `avHydrate` returns `unknown`, so the value is typed for `SWRConfig` where it is used. SWR runs its fetcher in the browser only, so `@/aventara/client` is enough here; the universal client works as well.
## Mistakes to avoid [#mistakes-to-avoid]
| Mistake | What happens |
| ------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| The query module imports `@/aventara/client` | The server prefetch is refused: `avClient from @/aventara/client runs in the browser only, ...` |
| The query module imports `@/aventara/server` | `next build` fails: a Client Component imports a `server-only` module. |
| No `instrumentation.ts` awaiting `ready()` | The universal call on the server is refused, naming `instrumentation.ts` and `ready()`. |
| `dehydrate` without `serializeData: avDehydrate` | React refuses to pass a decimal from the Server Component. |
| No `deserializeData: avHydrate` in the browser | The query's data holds tagged objects (`{ "$av": "datetime", "v": "..." }`) instead of a `Date`. |
| One `QueryClient` shared by every request on the server | One user's prefetched data can reach another. Make one per request. |
## See also [#see-also]
* [Next.js host](/framework/docs/nextjs-host): the three clients and when to use each.
* [Next.js API reference](/framework/docs/next-api-reference#avdehydrate): `avDehydrate` and `avHydrate`.
* [Enums and scalar types](/framework/docs/enums-and-scalars): the value types.