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.jsximport {
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.jsximport 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.jsximport { 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.mjsexport 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.jsximport 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.jsximport { 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.