IntegrationsEdit this page.md

TanStack Query

@lazarv/react-server works with TanStack Query (formerly React Query) to provide powerful data fetching with server-side prefetching and client-side hydration. You can prefetch queries in server components and seamlessly hydrate the data on the client, avoiding unnecessary re-fetches.

Install TanStack Query in your project:

pnpm add @tanstack/react-query

To use TanStack Query, you need to create a QueryClient and wrap your app in a QueryClientProvider. Since QueryClientProvider relies on React context, it must be a client component.

Create a query client factory that handles both server and browser environments:

app/get-query-client.jsx
import { defaultShouldDehydrateQuery, isServer, QueryClient, } from "@tanstack/react-query"; function makeQueryClient() { return new QueryClient({ defaultOptions: { queries: { // With SSR, set some default staleTime above 0 // to avoid refetching immediately on the client staleTime: 60 * 1000, }, dehydrate: { // include pending queries in dehydration shouldDehydrateQuery: (query) => defaultShouldDehydrateQuery(query) || query.state.status === "pending", }, }, }); } let browserQueryClient = undefined; export function getQueryClient() { if (isServer) { // Server: always make a new query client return makeQueryClient(); } else { // Browser: make a new query client if we don't already have one if (!browserQueryClient) browserQueryClient = makeQueryClient(); return browserQueryClient; } }

Create a client component that provides the QueryClient to the rest of your app:

app/providers.jsx
"use client"; import { isServer, QueryClient, QueryClientProvider, } from "@tanstack/react-query"; function makeQueryClient() { return new QueryClient({ defaultOptions: { queries: { staleTime: 60 * 1000, }, }, }); } let browserQueryClient = undefined; function getQueryClient() { if (isServer) { return makeQueryClient(); } else { if (!browserQueryClient) browserQueryClient = makeQueryClient(); return browserQueryClient; } } export default function Providers({ children }) { const queryClient = getQueryClient(); return ( <QueryClientProvider client={queryClient}> {children} </QueryClientProvider> ); }

Then wrap your app in the Providers component from your root layout:

app/layout.jsx
import Providers from "./providers"; export default function RootLayout({ children }) { return ( <html lang="en" suppressHydrationWarning> <head /> <body suppressHydrationWarning> <Providers>{children}</Providers> </body> </html> ); }

The key advantage of using TanStack Query with @lazarv/react-server is the ability to prefetch data in server components and hydrate it on the client. This means data is available instantly without a loading state.

In a server component, use the query client to prefetch data and wrap your client components in a HydrationBoundary:

app/page.jsx
import { dehydrate, HydrationBoundary } from "@tanstack/react-query"; import { getPosts } from "./get-posts"; import { getQueryClient } from "./get-query-client"; import Posts from "./posts"; export default function PostsPage() { const queryClient = getQueryClient(); queryClient.prefetchQuery({ queryKey: ["posts"], queryFn: getPosts, }); return ( <HydrationBoundary state={dehydrate(queryClient)}> <Posts /> </HydrationBoundary> ); }

Client components can use useSuspenseQuery (or useQuery) with the same query keys. When the data was prefetched on the server, it will be immediately available without a loading state:

app/posts.jsx
"use client"; import { useSuspenseQuery } from "@tanstack/react-query"; import { getPosts } from "./get-posts"; export default function Posts() { const { data } = useSuspenseQuery({ queryKey: ["posts"], queryFn: getPosts, }); return ( <ul> {data.map((post) => ( <li key={post.id}>{post.title}</li> ))} </ul> ); }

To share data fetching logic between server and client, you can create isomorphic data fetchers that detect the runtime environment:

app/get-posts.mjs
export async function getPosts() { if (typeof document === "undefined") { // Server: import data directly const { default: posts } = await import("../data/posts.json"); return posts; } else { // Client: fetch from API route const res = await fetch("/api/posts"); return res.json(); } }

You can combine this with API routes using file-system routing:

app/api/GET.posts.jsx
import posts from "../../data/posts.json"; export default async function GET() { return new Response(JSON.stringify(posts), { status: 200, headers: { "Content-Type": "application/json", }, }); }

You can nest HydrationBoundary components in separate server components to prefetch different sets of data. This is useful for composing multiple data dependencies:

app/comments-server.jsx
import { dehydrate, HydrationBoundary } from "@tanstack/react-query"; import Comments from "./comments"; import { getComments } from "./get-comments"; import { getQueryClient } from "./get-query-client"; export default function CommentsServerComponent() { const queryClient = getQueryClient(); queryClient.prefetchQuery({ queryKey: ["comments"], queryFn: getComments, }); return ( <HydrationBoundary state={dehydrate(queryClient)}> <Comments /> </HydrationBoundary> ); }

Check out the TanStack Query example to see a complete example of using TanStack Query with @lazarv/react-server.