Production React apps no longer render everything in the browser. This lesson covers the server/client rendering split that powers modern frameworks: Server Components, Client Components, streaming with Suspense, hydration, and Server Actions.
1Beyond Client-Side Rendering
Modern React applications split their component tree across the server and the client. Server Components render once on the server and produce plain HTML with no client JavaScript cost, while Client Components hydrate in the browser to remain interactive. A single page typically mixes both kinds of components in the same tree.
2Server Components
A Server Component runs exclusively on the server, can access backend resources like a database directly, and contributes zero JavaScript to the client bundle since it never re-renders in the browser. It's the default kind of component in frameworks built on the App Router model, and it's ideal for data-heavy, non-interactive UI.
3Client Components and 'use client'
Any component that needs state, effects, event handlers, or browser-only APIs must be marked with the 'use client' directive at the top of its file. Client Components are still rendered to HTML on the server for the first paint, then hydrated in the browser so their interactivity becomes active.
4Streaming with Suspense
Rather than waiting for every piece of data before sending a response, React can stream a page: fast-rendering sections reach the browser immediately, while slower sections wrapped in a Suspense boundary stream in afterward, replacing their fallback once ready. This lets users see meaningful content far sooner than a fully blocking render would allow.
5Hydration and Selective Hydration
Hydration attaches React's event handlers and internal state to already-rendered server HTML, turning static markup into an interactive application. Selective hydration lets React prioritize hydrating the part of the page a user is actively interacting with, rather than requiring the entire page to finish hydrating before anything responds.
6Server Actions: 'use server'
A function marked with 'use server' always executes on the server, even when it's invoked from an interactive Client Component, such as through a form submission. React manages the network call automatically, letting a Client Component call server-side logic directly instead of requiring a hand-written API route and fetch request.
7Step-by-Step Breakdown
Beyond Client-Side Rendering. So far, every component you've built has run entirely in the browser. Modern React apps split rendering across the server and the client: some components render once on the server and ship as plain HTML, others hydrate and stay interactive in the browser. Understanding this split is essential for building fast, production-grade apps.
Server Components. A React Server Component (RSC) runs only on the server, has zero JavaScript bundle cost on the client, and can directly access backend resources like a database or filesystem. It renders once, produces a description of UI, and never re-renders in the browser — there's no client-side interactivity available inside it.
Client Components and 'use client'. Any component that needs interactivity — state, effects, event handlers, browser APIs — must be a Client Component, marked with the 'use client' directive at the top of the file. Client Components still get server-rendered for the first paint, then hydrate in the browser to become interactive.
Which kind of component can directly query a database, but can never use useState?
- →A Server Component
- →A Client Component
Streaming with Suspense. Instead of waiting for every piece of data before sending any HTML, modern React can stream the page: fast parts render and reach the browser immediately, while slower parts are wrapped in <Suspense> and stream in later, filling their fallback placeholder once ready. The user sees content sooner instead of a blank screen.
Hydration and Selective Hydration. Hydration is the process where React attaches event handlers and internal state to server-rendered HTML, turning static markup into an interactive app. Selective hydration means React can hydrate the parts of the page a user is actually interacting with first, even if other Suspense boundaries are still streaming in — interactivity doesn't have to wait for the entire page.
What is 'selective hydration' designed to improve?
- →Letting the parts of the page a user interacts with become interactive first
- →Making the initial HTML file smaller
Server Actions: 'use server'. A Server Action, marked with 'use server', is a function that always runs on the server, even when called from an interactive Client Component — for example, from a form submission. React handles the network round-trip automatically, so a Client Component can call server logic directly without you writing a manual API route and fetch call.
Mastery Achieved. You now understand the modern React rendering model: Server Components for zero-bundle data access, Client Components for interactivity, streaming with Suspense to show content sooner, hydration (and selective hydration) to make it interactive, and Server Actions to call server logic directly. This is the foundation frameworks like Next.js are built on.
Level Up 🚀
Advanced cheat sheets, SEO tricks, and interview prep for this topic.
Browser Support
Streaming and hydration are fully supported in all modern browsers.
Fully supported.
Fully supported.
Fully supported.
Accessibility (A11y)
1Suspense Fallbacks Need Accessible Loading Semantics
A loading fallback shown during streaming should use appropriate ARIA (like role='status' or aria-live='polite') so screen reader users are informed that content is on its way, rather than assuming the page is broken.
2Selective Hydration Shouldn't Leave Controls Silently Unresponsive
A control that's visible but not yet hydrated can appear clickable to a screen reader or keyboard user before it actually responds — avoid rendering interactive-looking elements inside slow Suspense boundaries without a clear pending indication.
SEO Implications
- 1
Server Components Ship Fully-Formed HTML to Crawlers
Because Server Components render on the server before any client JavaScript runs, crawlers see complete, indexable markup immediately, without depending on JavaScript execution to reveal content.
- 2
Streaming Reduces Time to First Byte for Meaningful Content
Sending fast-rendering sections immediately while slower ones stream in later improves perceived load speed, which is a factor in Core Web Vitals metrics that influence search ranking.
Best Practices
Default to Server Components, Opt Into Client Only When Needed
Keep components as Server Components unless they specifically need state, effects, or event handlers — this minimizes client bundle size and maximizes the amount of UI that costs nothing on the client.
Wrap Genuinely Slow Data Fetches in Suspense
Isolate slow, non-critical sections (like a recommendations widget) in their own Suspense boundary so they don't block the rest of the page's initial HTML from streaming to the browser.
Frequent Bugs
Calling useState inside a component throws an error about it not being a Client Component.
Add 'use client' at the top of the file. Any component using state, effects, refs, or event handlers must be explicitly marked as a Client Component — this is not automatic.
A Server Component tries to import and render a component that itself uses 'use client', and everything downstream unexpectedly becomes client-rendered.
Client Components can be rendered from Server Components, but the reverse boundary matters: once you cross into a Client Component, its own children are also client-rendered unless explicitly passed in as already-rendered Server Component children via props/children.
Real-World Examples
A Product Page with Server-Rendered Data and a Client Cart Button
A product detail page is a Server Component that fetches pricing and inventory directly from a database, while the 'Add to Cart' button is a small Client Component marked with 'use client' so it can hold local state and call a Server Action on click.
// page.tsx (Server Component)
async function ProductPage({ id }) {
const product = await db.products.get(id);
return (
<div>
<h1>{product.name}</h1>
<AddToCartButton productId={product.id} />
</div>
);
}