Home/Learn/Next.js/Client-Side Data Alongside Server Components

Client-Side Data Alongside Server Components

Advanced
Data & Caching

Server 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.

First paint and SEO vs interaction and polling
// 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.

initialData, or a HydrationBoundary
// 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.

RSC covers reads; libraries cover interaction
// 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()                    // yes

Key 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 Aria
1

When do you still need client-side data fetching in an App Router app?

Medium
2

How do you stop a client query refetching data the server already rendered?

Hard
3

Why is calling your own /api route from a server component wasteful?

Medium

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.

Loading discussion…