Client-Side Data Alongside Server Components
AdvancedServer components do not remove the need for client fetching. Knowing which of the two a piece of data belongs to — and how to hand data from one to the other — is the real skill.
Overview
A common overcorrection after learning RSC is to try to do everything on the server, then discover that a search box that filters as you type, or a list that polls, is miserable when every keystroke is a server round trip. Server components own the initial render; client-side data libraries still own anything interactive, polling, optimistic or offline. The two compose well once you know the seam: the server renders the first payload and hands it to the client cache as initial data, so there is no duplicate fetch and no loading flash.
Which Side Owns It
A decision list, not a preference.
// SERVER COMPONENT when the data is:
// needed for the first paint or for SEO
// the same for everyone, or cacheable per user
// large, or expensive to compute
// from a source with a secret (a database, a keyed API)
// CLIENT when the data:
// changes in response to interaction (search-as-you-type, filters)
// polls or streams (a live job status, a leaderboard)
// is optimistically updated (a like, a toggle)
// depends on a browser API (geolocation, media queries)
// The tell that you have it wrong: a server round trip on every
// keystroke, or a spinner on a page that could have been prerendered.
// A search page usually wants BOTH: the initial results rendered on
// the server for SEO and first paint, then client-side fetching as
// the user types.Handing Server Data to the Client Cache
initialData, so the client does not refetch what the server already sent.
// Server component — fetches and renders
export default async function ProblemsPage() {
const initial = await getProblems({ page: 1 })
return <ProblemsClient initialData={initial} />
}
// Client component — takes over from there
'use client'
export function ProblemsClient({ initialData }) {
const [page, setPage] = useState(1)
const { data } = useQuery({
queryKey: ['problems', page],
queryFn: () => fetchProblems({ page }),
initialData: page === 1 ? initialData : undefined, // no refetch on mount
placeholderData: keepPreviousData,
})
...
}
// Without initialData the client refetches page 1 immediately on
// mount — a wasted request and a flash of loading state under
// content that was already on screen.
// The alternative for a whole tree is hydrating the query client from
// a server-side prefetch:
const qc = new QueryClient()
await qc.prefetchQuery({ queryKey: ['problems'], queryFn: getProblems })
return <HydrationBoundary state={dehydrate(qc)}><Problems /></HydrationBoundary>Do You Need the Library at All
What the framework already covers, and when it does not.
// Next covers a surprising amount without a data library:
// initial fetch server component
// mutation + refresh server action + revalidatePath
// loading state loading.tsx / Suspense
// error state error.tsx
// navigation caching the router cache
// So for a mostly-read application, RSC plus server actions is often
// the whole answer, and adding TanStack Query is extra machinery.
// You want the library when you have:
// polling or realtime updates
// infinite scroll with cached pages
// optimistic updates beyond what useOptimistic covers
// the same data on many screens, invalidated together
// offline or retry behaviour
// Do not put server-fetched data into client state "to be safe" —
// you then own two copies that can disagree, which is exactly the
// bug the server component was avoiding.
// And do not call your own Next route handler from a server component
// to reach your own database. That is an HTTP round trip to yourself:
const res = await fetch('http://localhost:3000/api/problems') // no
const problems = await db.problem.findMany() // yesKey Points to Remember
- 1Server components own the first paint and SEO; client fetching owns interaction, polling and optimistic updates
- 2Pass server-fetched data as initialData so the client cache does not refetch it on mount
- 3HydrationBoundary transfers a whole prefetched cache from server to client
- 4For a read-heavy app, server components plus server actions often remove the need for a data library entirely
- 5Never fetch your own route handler from a server component — that is an HTTP round trip to yourself
Interview Questions
Sign in to ask AriaWhen do you still need client-side data fetching in an App Router app?
How do you stop a client query refetching data the server already rendered?
Why is calling your own /api route from a server component wasteful?
Ask Aria about Client-Side Data Alongside Server Components
Your personal AI tutor — ask anything about this concept
Revision Status
Personal Notes
Sign in to save personal notes for this topic.
Discussion
Sign in to join the discussion.