SSR & Prefetching
Server-side rendering and React Server Components require prefetching data on the server. @navios/react-query provides utilities to simplify this process.
Overview
When rendering on the server, you need to:
- Create a
QueryClientinstance - Prefetch data before rendering
- Dehydrate the cache state
- Hydrate on the client
The prefetch helpers abstract away the boilerplate and provide a type-safe API.
Basic Usage
Creating a Prefetch Helper
import { createPrefetchHelper } from '@navios/react-query'
// Your existing query
const getUser = client.query({
method: 'GET',
url: '/users/$userId',
responseSchema: userSchema,
processResponse: (data) => data,
})
// Create prefetch helper
const userPrefetch = createPrefetchHelper(getUser)
Using in Server Components (Next.js App Router)
import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query'
async function UserPage({ params }: { params: { userId: string } }) {
const queryClient = new QueryClient()
await userPrefetch.prefetch(queryClient, {
urlParams: { userId: params.userId },
})
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<UserProfile userId={params.userId} />
</HydrationBoundary>
)
}
Using in getServerSideProps (Next.js Pages Router)
export async function getServerSideProps({ params }) {
const queryClient = new QueryClient()
await userPrefetch.prefetch(queryClient, {
urlParams: { userId: params.userId },
})
return {
props: {
dehydratedState: dehydrate(queryClient),
},
}
}
function UserPage({ dehydratedState }) {
return (
<HydrationBoundary state={dehydratedState}>
<UserProfile />
</HydrationBoundary>
)
}
Prefetch Helper Methods
prefetch
Prefetch data into the query cache. Does not return data.
await userPrefetch.prefetch(queryClient, {
urlParams: { userId: '123' },
})
ensureData
Ensure data exists in cache. Returns the data if found, otherwise fetches it.
const userData = await userPrefetch.ensureData(queryClient, {
urlParams: { userId: '123' },
})
console.log('User:', userData.name)
getQueryOptions
Get raw query options for advanced use cases.
const options = userPrefetch.getQueryOptions({
urlParams: { userId: '123' },
})
// Customize before prefetching
await queryClient.prefetchQuery({
...options,
staleTime: 60000, // Custom stale time for prefetch
})
prefetchMany
Prefetch multiple queries with the same helper in parallel.
await userPrefetch.prefetchMany(queryClient, [
{ urlParams: { userId: '1' } },
{ urlParams: { userId: '2' } },
{ urlParams: { userId: '3' } },
])
Batch Prefetching
createPrefetchHelpers
Create multiple prefetch helpers at once from a record of queries.
import { createPrefetchHelpers } from '@navios/react-query'
const queries = {
user: client.query({
method: 'GET',
url: '/users/$userId',
responseSchema: userSchema,
}),
posts: client.query({
method: 'GET',
url: '/users/$userId/posts',
responseSchema: postsSchema,
}),
profile: client.query({
method: 'GET',
url: '/users/$userId/profile',
responseSchema: profileSchema,
}),
}
const prefetchers = createPrefetchHelpers(queries)
// Use each prefetcher
await prefetchers.user.prefetch(queryClient, { urlParams: { userId } })
await prefetchers.posts.prefetch(queryClient, { urlParams: { userId } })
prefetchAll
Prefetch multiple different queries in parallel.
import { prefetchAll } from '@navios/react-query'
const userPrefetch = createPrefetchHelper(getUser)
const postsPrefetch = createPrefetchHelper(getUserPosts)
const profilePrefetch = createPrefetchHelper(getUserProfile)
async function DashboardPage({ userId }: { userId: string }) {
const queryClient = new QueryClient()
await prefetchAll(queryClient, [
{ helper: userPrefetch, params: { urlParams: { userId } } },
{ helper: postsPrefetch, params: { urlParams: { userId }, params: { limit: 10 } } },
{ helper: profilePrefetch, params: { urlParams: { userId } } },
])
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<Dashboard userId={userId} />
</HydrationBoundary>
)
}
Complete Example
Here's a complete example with Next.js App Router:
// lib/queries.ts
import { builder } from '@navios/builder'
import { create } from '@navios/http'
import { declareClient, createPrefetchHelper } from '@navios/react-query'
import { z } from 'zod'
const api = builder({})
api.provideClient(create({ baseURL: process.env.API_URL }))
const client = declareClient({ api })
export const getUser = client.query({
method: 'GET',
url: '/users/$userId',
responseSchema: z.object({
id: z.string(),
name: z.string(),
email: z.string(),
}),
processResponse: (data) => data,
})
export const getUserPosts = client.query({
method: 'GET',
url: '/users/$userId/posts',
querySchema: z.object({
page: z.number().default(1),
limit: z.number().default(10),
}),
responseSchema: z.object({
posts: z.array(z.object({
id: z.string(),
title: z.string(),
})),
total: z.number(),
}),
processResponse: (data) => data,
})
export const userPrefetch = createPrefetchHelper(getUser)
export const postsPrefetch = createPrefetchHelper(getUserPosts)
// app/users/[userId]/page.tsx
import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query'
import { userPrefetch, postsPrefetch } from '@/lib/queries'
import { UserProfile } from '@/components/UserProfile'
export default async function UserPage({ params }: { params: { userId: string } }) {
const queryClient = new QueryClient()
// Prefetch in parallel
await Promise.all([
userPrefetch.prefetch(queryClient, {
urlParams: { userId: params.userId },
}),
postsPrefetch.prefetch(queryClient, {
urlParams: { userId: params.userId },
params: { page: 1, limit: 10 },
}),
])
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<UserProfile userId={params.userId} />
</HydrationBoundary>
)
}
// components/UserProfile.tsx
'use client'
import { getUser, getUserPosts } from '@/lib/queries'
export function UserProfile({ userId }: { userId: string }) {
// Data is already in cache from server prefetch
const user = getUser.useSuspense({ urlParams: { userId } })
const posts = getUserPosts.useSuspense({
urlParams: { userId },
params: { page: 1, limit: 10 },
})
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
<h2>Posts ({posts.total})</h2>
<ul>
{posts.posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</div>
)
}
Error Handling
Errors during prefetch can be handled in several ways:
Silent Failures
Let errors fail silently - the client will re-fetch:
try {
await userPrefetch.prefetch(queryClient, { urlParams: { userId } })
} catch {
// Ignore - client will fetch
}
Use ensureData with Error Boundary
try {
const user = await userPrefetch.ensureData(queryClient, { urlParams: { userId } })
} catch (error) {
// Handle error (e.g., redirect to 404)
notFound()
}
Best Practices
Parallel Prefetching
Always prefetch independent queries in parallel:
// ✅ Good - parallel
await Promise.all([
userPrefetch.prefetch(queryClient, { urlParams: { userId } }),
postsPrefetch.prefetch(queryClient, { urlParams: { userId } }),
])
// ❌ Bad - sequential
await userPrefetch.prefetch(queryClient, { urlParams: { userId } })
await postsPrefetch.prefetch(queryClient, { urlParams: { userId } })
Create New QueryClient Per Request
// ✅ Good - new QueryClient per request
async function Page() {
const queryClient = new QueryClient()
// ...
}
// ❌ Bad - shared QueryClient
const queryClient = new QueryClient()
async function Page() {
// Uses shared instance
}
Match Server and Client Parameters
Ensure the same parameters are used on server and client:
// Server
await userPrefetch.prefetch(queryClient, {
urlParams: { userId },
params: { page: 1 },
})
// Client - must match
const user = getUser.useSuspense({
urlParams: { userId },
params: { page: 1 }, // Same params
})
Next Steps
- Queries - Learn about queries
- Suspense - Use Suspense for data fetching
- API Reference - Full API documentation