React

Server Components & SSR

14 Questions

Server-Side Rendering (SSR) renders React components to complete HTML on the server and sends the finished page to the browser, so users see content immediately without waiting for JavaScript to execute. This provides faster first meaningful paint, better SEO (search engines receive fully rendered HTML), and a usable page before JavaScript loads. After receiving the HTML, the browser downloads JavaScript and React hydrates the DOM — attaching event handlers and making the page interactive. The trade-off is that each request requires server computation rather than serving a cached static file.

Why it matters: SSR means users see content faster and search engines can index the page — pure client-side rendering leaves them with a blank page until JavaScript loads.

Real applications: E-commerce product pages, blog posts, news articles — any content where time-to-first-content and SEO matter.

Common mistakes: Using browser-only APIs (window, document) directly in server-rendered components — they don't exist on the server and will crash the render.

React Server Components (RSC), introduced with React 18, are components that run exclusively on the server and never ship their code to the browser. They can directly access databases, file systems, and backend services without needing API endpoints. The rendered output — HTML or a special RSC payload — is sent to the client, but zero component JavaScript is included in the browser bundle. This reduces bundle size significantly and enables efficient co-located data fetching at the individual component level.

Why it matters: Server Components reduce the client bundle size significantly — all their code stays on the server and never ships to the browser.

Real applications: A product listing component that fetches from the database directly — no API endpoint needed, no client-side fetching, no loading state.

Common mistakes: Adding 'use client' to every component out of habit — Server Components should be the default; only add 'use client' when you need interactivity.

Hydration is the process where React attaches event handlers, state, and effects to server-rendered HTML, transforming a static HTML page into a fully interactive React application. React reuses the existing DOM nodes rather than rebuilding them, so the process is invisible to the user. If the client render output doesn't match the server HTML, React detects a hydration mismatch and warns while re-rendering the affected subtree. Hydration is what makes SSR pages feel instant — users see content from the server immediately, interactivity arrives shortly after.

Why it matters: Hydration is what lets SSR pages feel instant — users see content immediately from the server HTML, then JavaScript makes it interactive without a visible rebuild.

Real applications: A server-rendered page loads with visible content; React hydrates it so buttons become clickable and forms become submittable without re-rendering the page.

Common mistakes: Causing hydration mismatches by using different logic on server vs client (like Date.now()) — React warns and can fail to hydrate correctly.

SSR (Server-Side Rendering) generates HTML on each request, delivering fresh user-specific content — ideal for dashboards, authenticated pages, and dynamic data. SSG (Static Site Generation) generates HTML at build time and serves the same pre-built page from a CDN — ideal for content that changes rarely and needs maximum performance. ISR (Incremental Static Regeneration) bridges the gap, regenerating static pages in the background at configurable intervals for the best of both worlds.

Why it matters: Choosing the wrong rendering strategy leads to either stale content (SSG for dynamic data) or slow performance (SSR for static pages).

Real applications: SSG for a marketing site or blog; SSR for a dashboard with user-specific data; ISR for a product catalog that updates occasionally.

Common mistakes: Using SSR for every page — pages that don't need fresh data on every request waste server resources and are slower than SSG.

Streaming SSR (React 18) uses HTTP streaming to send HTML chunks to the browser as each part renders, rather than waiting for the entire page to finish. This eliminates the bottleneck where a single slow component blocks the entire page from loading. Combined with Suspense boundaries, fast sections like navigation appear immediately while slow data-fetching sections stream in progressively. This dramatically improves Time to First Byte and perceived loading performance for complex pages.

Why it matters: Streaming eliminates the "wait for the slowest component" problem in SSR — navigation and header appear instantly while the feed loads.

Real applications: A news site renders the navigation and above-the-fold content first, then streams in the article body as the database query completes.

Common mistakes: Not wrapping slow server components in Suspense — without Suspense boundaries, streaming can't progressively reveal fast sections while waiting for slow ones.

In the React Server Components architecture, 'use client' is a directive placed at the top of a file to mark it as a Client Component — it runs in the browser and can use hooks, event handlers, and browser APIs. Without this directive, Next.js App Router treats components as Server Components by default. The directive creates a client boundary: that file and all its imports become browser-side code. Keep Client Components as leaf nodes to minimize the JavaScript shipped to the browser.

'use client';

import { useState } from 'react';

export function Counter() {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount(c + 1)}>{count}</button>;
}

Why it matters: Without the directive, React assumes components are Server Components — adding 'use client' opts in the component to run in the browser with hooks and events.

Real applications: Interactive elements like dropdowns, forms, modals, and any component using useState or useEffect need 'use client'.

Common mistakes: Adding 'use client' to a parent component unnecessarily — it converts the entire subtree to client-side; keep Client Components as leaf components when possible.

'use server' marks an async function as a Server Action — a function that runs on the server but can be called directly from Client Components, including as HTML form action values. React handles the network serialization automatically: calling a Server Action from the client triggers a secure server-side POST request transparently. This eliminates separate API routes for simple mutations while keeping credentials and database logic server-side only. Always validate all inputs since they originate from untrusted client code.

'use server';

export async function saveUser(formData) {
  const name = formData.get('name');
  await db.users.create({ name });
}

Why it matters: Server Actions let you call server-side logic (like writing to a database) directly from a form without building a separate API endpoint.

Real applications: Form submissions that save to a database, file uploads, sending emails — any mutation that should run securely on the server.

Common mistakes: Forgetting to validate input in Server Actions — the function runs on the server but is callable from client code, so treat all inputs as untrusted.

Incremental Static Regeneration (ISR) regenerates static pages in the background after a configurable time interval, giving you CDN-speed delivery with automatically refreshed content — no full rebuild needed when data changes. While a page is regenerating, visitors receive the previous cached version without any slowdown — the new version replaces the cache only when ready. ISR is the best strategy when content changes on a schedule: product prices, blog post counts, or inventory levels that need to be fresh but not real-time.

Why it matters: ISR gives you the performance of static pages with periodic freshness — the best of both SSG and SSR without the trade-offs of either extreme.

Real applications: A product catalog page with revalidate: 60 — always fast to load (static), but product prices and stock update every minute.

Common mistakes: Setting the revalidation interval too short — very short intervals approach the overhead of SSR without the benefit of serving cached responses.

Server Components run in a Node.js environment during server rendering and have no access to browser-specific features or React's client-side APIs. These limitations are fundamental: Server Components produce static output once, have no reactive lifecycle, and no client state. Any part of the UI needing these capabilities must be extracted into a separate Client Component file with the 'use client' directive at the top.

  • Use hooks (useState, useEffect, etc.)
  • Add event handlers (onClick, onChange)
  • Use browser APIs (window, document)
  • Maintain client-side state

For interactivity, use Client Components with 'use client'.

Why it matters: Knowing the limitations tells you exactly when a Server Component boundary needs to move or a Client Component needs to be extracted.

Real applications: A product page can be a Server Component for data fetching, but the "Add to Cart" button must be extracted as a Client Component for the click handler.

Common mistakes: Trying to add onClick to a Server Component — React will throw a clear error; extract interactive parts to a separate 'use client' file.

In Next.js App Router, all components default to Server Components — add 'use client' only to files needing hooks, event handlers, or browser APIs. Server Components can import and render Client Components, but Client Components cannot import Server Components (they're server-only). Data fetching happens directly with async/await in Server Components — no useEffect needed. This default-to-server model minimizes JavaScript shipped to the browser automatically.

Why it matters: The default-to-server model means you only opt into client-side JavaScript when you actually need it — smaller bundles and better performance by default.

Real applications: Next.js App Router pages fetch data in the Server Component and pass it as props to small Client Component islands for interactive pieces.

Common mistakes: Trying to import a Client Component into a Server Component that passes server-only data — serializable props (JSON) work; non-serializable values like functions don't.

getServerSideProps in the Pages Router is a separate exported function that runs on the server per request and passes data as props to the page component — data fetching and rendering are decoupled. Server Components in the App Router unify them: the component itself is async and fetches its own data directly via await. This enables component-level data co-location and fine-grained loading states at the individual component level, not just at the page level.

// Pages Router — getServerSideProps
export async function getServerSideProps({ params }) {
  const user = await fetchUser(params.id);
  return { props: { user } };
}
export default function Page({ user }) { return <h1>{user.name}</h1>; }

// App Router — Server Component
export default async function Page({ params }) {
  const user = await fetchUser(params.id); // runs on server directly
  return <h1>{user.name}</h1>;
}

Server Components eliminate the data-fetching boilerplate of getServerSideProps and enable more fine-grained component-level data loading.

Why it matters: Server Components let any component fetch its own data directly — you don't need to thread data through page-level props anymore.

Real applications: A deeply nested avatar component can fetch the user's image URL itself in App Router — no prop drilling from the page level.

Common mistakes: Migrating getServerSideProps code to App Router components without reviewing it — the patterns are different, not just a copy-paste.

Server Actions are async functions marked with 'use server' that run on the server but can be called directly from Client Components as form actions or event callbacks. React handles the network serialization transparently — calling a Server Action from the client sends a POST request to the server without any manual fetch code. This eliminates separate API routes for mutations, and keeps database credentials and business logic server-side. Always validate and sanitize Server Action inputs since they receive untrusted data from clients.

// app/actions.js
'use server';

export async function createTodo(formData) {
  const title = formData.get('title');
  await db.todos.create({ title, userId: getUser().id });
  revalidatePath('/todos'); // refresh cached page data
}

// Client Component — calls the server action on submit
'use client';
import { createTodo } from './actions';

export function TodoForm() {
  return (
    <form action={createTodo}>
      <input name="title" />
      <button type="submit">Add</button>
    </form>
  );
}

Server Actions run securely on the server — database credentials and secrets are never exposed to the client bundle.

Why it matters: Server Actions eliminate the need for a separate API layer for simple mutations — the form submits directly to server-side code.

Real applications: A "Subscribe" form that saves an email to a database — no API route needed, just a Server Action function called directly from the form.

Common mistakes: Skipping input validation — even though Server Actions run on the server, they accept user input from the network and must validate every field.

A hydration mismatch occurs when the HTML React renders on the server differs from what it renders on the client during hydration. React logs a warning and re-renders the affected subtree client-side, defeating SSR's performance benefit. The most common causes are non-deterministic values (Math.random(), Date.now()), browser-only APIs checked at render time, and typeof window conditionals. Fix by deferring dynamic values to after mount using useEffect and a mounted state flag.

// ❌ Causes mismatch — Math.random() produces different values server/client
function Component() {
  return <div id={Math.random()}>Content</div>;
}

// ❌ Causes mismatch — Date.now() differs between server and client
function Timestamp() {
  return <time>{new Date().toLocaleString()}</time>;
}

// ✅ Fix — render dynamic content only after mount
function Timestamp() {
  const [time, setTime] = useState('');
  useEffect(() => setTime(new Date().toLocaleString()), []);
  return <time>{time}</time>;
}

Common causes: non-deterministic values (random IDs, dates), browser-only APIs, or conditional rendering based on typeof window.

Why it matters: Hydration mismatches cause React to discard and re-render the server HTML on the client — defeating the performance benefit of SSR.

Real applications: A timestamp that uses Date.now() on both server and client will always mismatch — initialize it client-side in useEffect instead.

Common mistakes: Using Math.random() or Date.now() directly in JSX — these produce different values on server and client; defer them to after mount.

Server Components improve security because their code and any secrets they use never reach the browser bundle. Database credentials, internal service URLs, and query logic stay server-side — there's no public API endpoint to attack or sensitive code to inspect in browser DevTools. This eliminates entire attack vectors: injection attacks and authentication bypass attempts against data-fetching endpoints have no target when data is fetched directly during server rendering. Handle authorization checks explicitly inside Server Components, since they run for every user request.

// This code NEVER reaches the browser bundle
async function SecretPage() {
  // ✅ Safe — DB_SECRET stays on server
  const conn = await db.connect(process.env.DB_SECRET);
  const data = await conn.query('SELECT * FROM sensitive_table');

  // Only the rendered HTML is sent to the browser
  return <DataTable rows={data} />;
}

// With a REST API (old approach)
// The API URL and auth logic are visible in the client bundle
fetch('/api/data', { headers: { Authorization: token } });

Server Components also reduce the attack surface by eliminating the need to expose database or internal service APIs to the public internet.

Why it matters: When database queries run in Server Components, you eliminate an entire class of vulnerabilities — there's no public API endpoint to attack.

Real applications: Customer data queries happen directly in the Server Component — no REST endpoint means no authentication bypass or injection attack vector on that data path.

Common mistakes: Logging sensitive data inside Server Components without realizing it — server logs can contain secrets if you console.log query results carelessly.