Skip to solution
hardFrontend

What is server-side rendering (SSR) and when would you use it with React?

767 views
01

Understand the problem

Question presented to candidate: "What does server-side rendering actually give you in a React app, and when would you not bother?"

What a strong answer should cover:

  • SSR runs your components on the server and sends real HTML, so the first response already contains content instead of an empty <div id="root">.
  • The server render is one pass with no lifecycle: state is initial, effects never run, refs are never attached. Anything that touches the DOM must be moved into an effect.
  • The HTML contains no event handlers. It is inert until the client bundle loads and hydrates it — SSR without hydration gives you a page that looks right and does nothing.
  • The wins are first contentful paint on slow networks and devices, and crawlers and link previews that read HTML rather than executing scripts.
  • The costs are real: server CPU per request, a more complex deployment, and code that must be safe to run without window or document.
  • renderToString is the blocking, legacy API and does not support Suspense. The streaming APIs — renderToPipeableStream on Node, renderToReadableStream on web runtimes — are what production uses.
  • SSR does not make the page interactive sooner on its own; hydration still has to happen, and a large bundle can make time-to-interactive worse than CSR.
  • Skip it for authenticated dashboards behind a login, internal tools, and anything where nothing is public and every user has a fast machine.

Clarifying questions expected:

  • "Is this content public and crawlable, or behind authentication?" — that usually decides it.
  • "Which metric are we actually trying to move: first paint, or interactivity?"

Code / implementation expected: Optional. Naming the right server API and pairing it with hydrateRoot is the substance.

ssrperformanceseonext.js
02

Attempt it yourself

Sketch your approach before reading the solution — that's what interviews test.

Nudge consolestandby

Stuck? Beam a request up — the console returns a conceptual nudge that guides your logic without spoiling the implementation.

03

Study the solution

Target Audience: Engineers preparing for React interviews — no prior SSR experience assumed. Difficulty: Hard

How to read this doc: Concepts are explained in plain language first, then tagged with 📌 Interview term:. Every server-render output below was executed against React 19.2.8 usin

Solution ready — 2 min read

Classified // press E to declassify

04

Explore the playground snippets

The same component rendered by the server API and by the client
Run Playground
import { useState, useEffect } from "react";
import { renderToString, renderToStaticMarkup } from "react-dom/server";

// One component, rendered two ways in the same page so the difference is
// visible rather than described.
function Counter() {
  const [count, setCount] = useState(7);
  const [mounted, setMounted] = useState("no");

  // On the server this never runs — which is why the server HTML below shows
  // count 7 and mounted "no", not the values this effect would set.
  useEffect(() => { setMounted("yes"); }, []);

  return (
    <button onClick={() => setCount((c) => c + 1)}>
      count {count} · effect ran: {mounted}
    </button>
  );
}

// Run the SERVER renderer at module scope, purely to show its output. It must
// NOT be called during another render — a server render nested inside a client
// render corrupts the hooks dispatcher and throws.
const serverHtml = renderToString(<Counter />);
const staticHtml = renderToStaticMarkup(<Counter />);

export default function App() {
  return (
    <div style={{ padding: 24, fontFamily: "system-ui", lineHeight: 1.7, maxWidth: 580 }}>
      <h4 style={{ margin: "0 0 6px" }}>What the server would send</h4>
      <pre style={{ background: "#f6f6f8", padding: 10, borderRadius: 8, fontSize: 12, overflowX: "auto" }}>
{"renderToString:\n  " + serverHtml + "\n\nrenderToStaticMarkup:\n  " + staticHtml}
      </pre>
      <p style={{ fontSize: 13, color: "#666", margin: "0 0 18px" }}>
        No <code>onclick</code> attribute anywhere — handlers are not
        serialisable. The count is 7 and the effect flag is "no", because the
        server has no commit phase to run effects in. The{" "}
        <code>&lt;!-- --&gt;</code> marker in the first one separates two
        adjacent text nodes so hydration can align them; the static renderer
        drops it because that output is never hydrated.
      </p>

      <h4 style={{ margin: "0 0 6px" }}>The same component, rendered by the client</h4>
      <Counter />
      <p style={{ fontSize: 13, color: "#666" }}>
        This one is live: the effect ran, so it says "yes", and clicking works.
        In a real SSR app the server HTML above would be sent first and painted
        immediately, then hydration would turn it into exactly this.
      </p>
    </div>
  );
}
05

Join the discussion

Discussion (0)

Sign in to join the discussion.

No responses yet. Be the first to share what you think.

Transmission complete // awaiting log

KEEP THE
STREAK ALIVE.

Dossier 87 of 119 decoded in the React.js track. One more won't hurt.

Back to track