Server Components & SSR

Difficulty: Advanced

Question

What are React Server Components? How do they differ from SSR and client components?

Answer

React Server Components and server-side rendering are two of the most important and most confused topics in modern React. They sound similar but solve different problems with different mechanisms. Let me untangle them clearly.

Start with the problem that SSR solves. A standard React application (client-side rendering, or CSR) works like this: the server sends a nearly empty HTML file containing a script tag, the browser downloads the JavaScript bundle, React boots up, and then renders your entire UI from scratch on the client side. Until the JavaScript downloads and executes, the user sees a blank page or a loading spinner. This has two major problems -- the initial load is slow because nothing is visible until the JS runs, and search engines may struggle to index content that only exists after JavaScript execution.

Server-side rendering fixes this by running your React components on the server. When a request comes in, the server executes your component tree, generates the complete HTML, and sends it to the browser. The user sees the fully rendered page almost immediately -- text, images, layout, everything -- before any JavaScript has loaded. This dramatically improves the First Contentful Paint metric and solves the SEO problem because search engine crawlers see real HTML.

But that server-rendered HTML is just static markup. Buttons do not respond to clicks, forms do not validate, nothing is interactive. It is like looking at a screenshot of your app. This is where hydration comes in. Hydration is the process where React takes over the server-rendered HTML and makes it interactive. React walks through the existing DOM, matches it against what it would render on the client, and attaches event listeners, sets up state, and connects hooks. The key insight is that React reuses the existing DOM nodes rather than recreating them -- this is faster than rendering from scratch.

Think of it like building a house. SSR delivers a fully built house -- walls, roof, windows, everything. But the electricity is not connected. Hydration is the electrician wiring up the light switches, outlets, and appliances. The house structure does not change, but everything becomes functional.

Now, React Server Components (RSC) are a fundamentally different concept. Server Components are components that run exclusively on the server and never run on the client. Their JavaScript code is never downloaded by the browser. Instead, the server executes the component and sends a serialized representation of the rendered output to the client. The client React runtime renders this output into the DOM.

The practical implications are significant. Server Components can directly access databases, file systems, environment variables, and server-only APIs. You do not need an API endpoint to fetch data -- you just query the database directly in the component function. Since their code never reaches the browser, you can use heavy server-only libraries (database ORMs, file processing, PDF generation) without increasing the client bundle size. A Server Component that imports a 500KB Markdown parser adds zero bytes to the client bundle.

In frameworks like Next.js App Router, all components are Server Components by default. When you need interactivity (state, effects, event handlers, browser APIs), you add the 'use client' directive at the top of the file. This creates a boundary -- everything in that file and everything it imports becomes client code.

The critical distinction between SSR and RSC is this: SSR renders components on the server for faster initial load, but then sends all the component JavaScript to the client for hydration. The components run on both server and client. RSC components ONLY run on the server -- no JavaScript is sent, no hydration happens. SSR is about performance (faster first paint). RSC is about architecture (keeping code server-only).

Server Actions complement Server Components by letting you define functions that run on the server but can be called from Client Components. You mark them with 'use server'. They are commonly used as form actions, enabling forms that work progressively -- with or without JavaScript enabled.

Hydration mismatches are the trickiest part of SSR. A mismatch happens when the HTML generated on the server does not match what React would render on the client. Common causes include: dates and times (different timezones on server vs client), random values (Math.random produces different results each time), browser-only APIs (window.innerWidth does not exist on the server), and conditional rendering based on client-only state. React 18 introduced useId to solve the ID mismatch problem -- it generates deterministic IDs that are stable across server and client.

One important caveat: React Server Components currently require a framework like Next.js to work. You cannot use them with a plain Vite or Create React App setup. The React team designed RSC as a framework-level feature that needs deep integration with the bundler and server.

Code examples

Server vs Client Components

// Server Component (default in Next.js App Router)
// NO 'use client' directive
// Runs ONLY on the server - never sent to client
async function ProductPage({ params }) {
  // Direct database access - no API needed!
  const product = await db.product.findUnique({
    where: { slug: params.slug }
  });

  // Can use server-only APIs
  const recommendations = await getMLRecommendations(product.id);

  return (
    <div>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
      <p>${product.price}</p>

      {/* Client component for interactivity */}
      <AddToCartButton productId={product.id} />

      {/* Server component for recommendations */}
      <RecommendationList items={recommendations} />
    </div>
  );
}

// Client Component - needs interactivity
// MUST have 'use client' directive
'use client';

import { useState } from 'react';

function AddToCartButton({ productId }) {
  const [added, setAdded] = useState(false);

  async function handleAdd() {
    await fetch('/api/cart', {
      method: 'POST',
      body: JSON.stringify({ productId }),
    });
    setAdded(true);
  }

  return (
    <button onClick={handleAdd} disabled={added}>
      {added ? 'Added!' : 'Add to Cart'}
    </button>
  );
}

Server Components fetch data directly with no API layer. Client Components handle user interaction. The product page HTML is streamed, but AddToCartButton JavaScript is sent for hydration.

SSR with Streaming

// Traditional SSR (React 17): Render all -> Hydrate all
// Server renders complete HTML string
// Client downloads ALL JS, hydrates entire page
// Problem: Nothing is interactive until EVERYTHING hydrates

// React 18 Streaming SSR: Progressive rendering
// index.html (Next.js App Router)
import { Suspense } from 'react';

async function DashboardPage() {
  // This data loads fast - streamed immediately
  const user = await getUser();

  return (
    <div>
      <h1>Welcome, {user.name}</h1>

      {/* Slow data - streamed when ready */}
      <Suspense fallback={<p>Loading analytics...</p>}>
        <AnalyticsPanel />
      </Suspense>

      {/* Another slow section - independent streaming */}
      <Suspense fallback={<p>Loading feed...</p>}>
        <ActivityFeed />
      </Suspense>
    </div>
  );
}

async function AnalyticsPanel() {
  // This might take 2 seconds
  const data = await fetchAnalytics();
  return <Chart data={data} />;
}

async function ActivityFeed() {
  // This might take 3 seconds
  const feed = await fetchFeed();
  return <Feed items={feed} />;
}

// Result:
// 0ms: User sees header + loading skeletons
// 2s: Analytics panel streams in and replaces skeleton
// 3s: Activity feed streams in and replaces skeleton
// Each section hydrates independently!

Streaming SSR with Suspense sends HTML progressively. Users see content as it becomes available instead of waiting for the slowest data source.

Server Actions and Data Mutations

// Server Action - runs on the server, called from client
'use server';

async function createPost(formData) {
  const title = formData.get('title');
  const content = formData.get('content');

  // Direct database access from the action
  const post = await db.post.create({
    data: { title, content, authorId: getCurrentUser().id }
  });

  // Revalidate cached data
  revalidatePath('/posts');
  redirect(`/posts/${post.slug}`);
}

// Client component using the server action
'use client';

import { useFormStatus } from 'react-dom';

function SubmitButton() {
  const { pending } = useFormStatus();
  return (
    <button type="submit" disabled={pending}>
      {pending ? 'Creating...' : 'Create Post'}
    </button>
  );
}

// Server component with the form
function CreatePostPage() {
  return (
    <form action={createPost}>
      <input name="title" required />
      <textarea name="content" required />
      <SubmitButton />
    </form>
  );
}

// The form works without JavaScript (progressive enhancement)
// With JS: smooth client-side submission
// Without JS: standard form POST to server action

Server Actions let client components call server-side functions directly. Forms with action={serverFunction} work with and without JavaScript, enabling progressive enhancement.

Key points

Concepts covered

Server Components, SSR, Hydration, Streaming, React Server Components