Skip to the solution
HardFrontend7 min read

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

767 views

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.

  • ssr
  • performance
  • seo
  • next.js

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

7 min read Β· 9 sections

Try it in the playground

Open any example in the playground to run and change it.

ReactThe same component rendered by the server API and by the client
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>
  );
}

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 87 of 119 in React.js

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All React.js questions
Up nextHard

React Compiler β€” how does auto-memoization work in React 19?

Continue