SSR Optimization
Prevent refetch waterfalls with server-side prefetching.
When you render on the server, data should already be in the cache when the client hydrates. Otherwise, TanStack Query refetches everything on mount — causing a visible loading flash.
Silgi provides prefetchQueries to solve this in one call.
The problem
Without prefetching, this is what happens:
- Server renders the page with loading spinners
- HTML is sent to the client
- React hydrates
- TanStack Query fires all queries
- User sees loading spinners until data arrives
With prefetching:
- Server fetches all data and seeds the query cache
- HTML is sent with the data already rendered
- React hydrates with cached data
- No loading spinners, no refetch waterfall
Setup
Create a server-side query client
Use createServerClient so prefetching runs in-process — no network round-trip.
import { } from 'silgi/client/server'
import { } from 'silgi/tanstack-query'
import { } from './router'
export function () {
const = (, {
: () => ({ : getDB() }),
})
return ()
}Prefetch in your server component or loader
import { dehydrate, HydrationBoundary, QueryClient } from "@tanstack/react-query"
import { createSSRUtils } from "../../server/query"
export default async function UsersPage() {
const queryClient = new QueryClient()
const utils = createSSRUtils()
// Prefetch the data server-side
await queryClient.prefetchQuery(
utils.users.list.queryOptions({ input: { limit: 20 } })
)
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<UserList />
</HydrationBoundary>
)
}import { dehydrate, HydrationBoundary, QueryClient } from "@tanstack/react-query"
import { createSSRUtils } from "../server/query"
import { createFileRoute } from "@tanstack/react-router"
export const Route = createFileRoute("/users")({
loader: async () => {
const queryClient = new QueryClient()
const utils = createSSRUtils()
await queryClient.prefetchQuery(
utils.users.list.queryOptions({ input: { limit: 20 } })
)
return { dehydratedState: dehydrate(queryClient) }
},
component: UsersPage,
})
function UsersPage() {
const { dehydratedState } = Route.useLoaderData()
return (
<HydrationBoundary state={dehydratedState}>
<UserList />
</HydrationBoundary>
)
}Use the same query on the client
The client component uses useQuery as normal. Because the data was prefetched, it is immediately available — no loading state.
'use client'
import { useQuery } from '@tanstack/react-query'
import { utils } from '../client/query'
export function UserList() {
// Data is already in the cache from SSR prefetching
const { data: users } = useQuery(utils.users.list.queryOptions({ input: { limit: 20 } }))
return (
<ul>
{users?.map((u) => (
<li key={u.id}>{u.name}</li>
))}
</ul>
)
}The query key must match exactly between server and client. Since both use utils.users.list.queryOptions({ input: { limit: 20 } }), TanStack Query recognizes the prefetched data and skips the refetch.
Prefetching multiple queries
Prefetch several queries in parallel with Promise.all:
await .([
queryClient.prefetchQuery(utils.users.list.queryOptions({ : { : 20 } })),
queryClient.prefetchQuery(utils.stats.summary.queryOptions({ : })),
])This is faster than sequential prefetching. All queries run concurrently on the server.
When not to prefetch
Skip prefetching for data that:
- Changes per user interaction — search results, filtered lists
- Is very large — the dehydrated state is serialized into the HTML
- Requires client-side context — data that depends on browser state (localStorage, URL params)
For these cases, let TanStack Query fetch on the client as usual.
What's next?
- TanStack Query — full query utils reference
- Client —
createServerClient()for in-process calls - Testing — use the same SSR utils in your test suite