Skip to solution
mediumFrontend

When would you choose a class component over a functional component with hooks?

610 views
01

Understand the problem

Question presented to candidate: "Is there any reason left to write a class component?"

What a strong answer should cover:

  • One real reason: an Error Boundary. Catching a render error requires getDerivedStateFromError or componentDidCatch, and there is still no hook equivalent. Every popular library wraps a class.
  • Beyond that, essentially none for new code. Hooks cover state, lifecycle, context and refs, and compose in ways class lifecycle methods cannot.
  • Classes are not deprecated and nothing was removedComponent and PureComponent are still exported. Migrating a working class component with no other reason is churn.
  • The structural argument: class lifecycle methods organise code by timing, so one concern is split across mount, update and unmount. Effects organise by concern, keeping setup and teardown together.
  • The reuse argument: the class era had HOCs and render props for sharing stateful logic; a custom hook does it without wrapper components or a nested tree.
  • A real behavioural difference to know: this.setState merges the object into state, while a useState setter replaces it. That is a genuine migration hazard.
  • Some modern APIs are hooks-onlyuseTransition, useDeferredValue, useSyncExternalStore, use — so a class cannot participate in concurrent features.
  • Honest exceptions: an existing class codebase, and a team standard.

Clarifying questions expected:

  • "New code or an existing codebase?" — the answers are completely different.
  • "Is this specifically about error boundaries?" — that is the one genuine case.

Code / implementation expected: Optional. Naming the error boundary exception is the substance.

class componentsfunctional componentshookslegacy
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 — assumes hooks. Difficulty: Medium

How to read this doc: Concepts are explained in plain language first, then tagged with 📌 Interview term:. Every claim below was executed against React 19.2.8 — the export check, the hook enumer

Solution ready — 2 min read

Classified // press E to declassify

04

Explore the playground snippets

The one case that still needs a class, and the setState difference that breaks ports
Run Playground
import React, { Component, useState } from "react";

// ── 1. THE ONE REASON: an error boundary needs a class ────────────────────
// There is no hook for this. React exports 18 use* hooks and none of them
// catches an error thrown by a child during render.
class ErrorBoundary extends Component {
  state = { error: null };
  static getDerivedStateFromError(error) { return { error }; }      // no hook equivalent
  componentDidCatch(error, info) { /* report(error, info.componentStack) */ }
  render() {
    if (this.state.error) {
      return (
        <div style={bad}>
          caught by a class boundary: {this.state.error.message}{" "}
          <button onClick={() => this.setState({ error: null })}>reset</button>
        </div>
      );
    }
    return this.props.children;
  }
}

function Boom({ broken }) {
  if (broken) throw new Error("render failed");
  return <div style={ok}>rendering fine</div>;
}

// ── 2. THE MIGRATION HAZARD: merge versus replace ─────────────────────────
class ClassState extends Component {
  state = { a: 1, b: 2 };
  render() {
    return (
      <Row
        label="class · this.setState({ a: 9 })"
        value={JSON.stringify(this.state)}
        onRun={() => this.setState({ a: 9 })}     // MERGES — b survives
      />
    );
  }
}

function HookState() {
  const [s, set] = useState({ a: 1, b: 2 });
  return (
    <Row
      label="hook · set({ a: 9 })"
      value={JSON.stringify(s)}
      onRun={() => set({ a: 9 })}                 // REPLACES — b is gone
    />
  );
}

function HookStateFixed() {
  const [s, set] = useState({ a: 1, b: 2 });
  return (
    <Row
      label="hook · set(prev => ({ ...prev, a: 9 }))"
      value={JSON.stringify(s)}
      onRun={() => set((prev) => ({ ...prev, a: 9 }))}   // the correct port
    />
  );
}

const ok = { background: "#f2f9f2", border: "1px solid #cde3cd", borderRadius: 6, padding: 10, fontSize: 13 };
const bad = { background: "#fdf0f0", border: "1px solid #e0b4b4", borderRadius: 6, padding: 10, fontSize: 13 };
const Row = ({ label, value, onRun }) => (
  <div style={{ display: "flex", gap: 8, alignItems: "center", fontSize: 13, padding: "3px 0" }}>
    <code style={{ minWidth: 250 }}>{label}</code>
    <button onClick={onRun}>run</button>
    <code style={{ color: "#4f46e5" }}>{value}</code>
  </div>
);

export default function App() {
  const [broken, setBroken] = useState(false);

  return (
    <div style={{ padding: 24, fontFamily: "system-ui", lineHeight: 1.6, maxWidth: 620 }}>
      <h4 style={{ margin: "0 0 6px" }}>1. The one thing only a class can do</h4>
      <button onClick={() => setBroken((b) => !b)} style={{ marginBottom: 8 }}>
        {broken ? "fix it" : "break it"}
      </button>
      <ErrorBoundary key={String(broken)}>
        <Boom broken={broken} />
      </ErrorBoundary>

      <h4 style={{ margin: "16px 0 6px" }}>2. Why a mechanical port drops data</h4>
      <div style={{ border: "1px solid #ddd", borderRadius: 8, padding: 10 }}>
        <ClassState />
        <HookState />
        <HookStateFixed />
      </div>

      <p style={{ fontSize: 13, color: "#666" }}>
        Press all three "run" buttons. The class keeps <code>b</code> because{" "}
        <code>this.setState</code> shallow-merges. The naive hook version loses
        it, with no error and no warning — which is the bug a bulk
        class-to-hooks migration introduces. The third row is the correct port.
      </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 54 of 119 decoded in the React.js track. One more won't hurt.

Back to track