Skip to the solution
MediumFrontend7 min read

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

610 views

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 removed β€” Component 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-only β€” useTransition, 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 components
  • functional components
  • hooks
  • legacy

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 Β· 10 sections

Try it in the playground

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

ReactThe one case that still needs a class, and the setState difference that breaks ports
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>
  );
}

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 54 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 nextMedium

What is 'prop drilling' and how can it be avoided?

Continue