Command Palette

Search for a command to run...

Hectal
PHASE 26Advanced ~28 min· topic 4 of 5

Topic 26.4

Class Components and Legacy Code

In one line

Before hooks, components with state were JavaScript classes with this.state, this.setState and lifecycle methods; you need to read them, explain how each maps to hooks, and migrate them safely, and error boundaries still have to be classes.

Think of it like this

An old car in the family. New cars have push-button starts, but your grandfather's car still has a key and a choke. You don't need to buy one, but if you're the mechanic you must know what each knob does and what it does in the new car. Class components are React's older model: most big codebases still contain them, interviewers still ask about them, and error boundaries can only be written as classes.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Class component
A component written as a class that extends Component and has a render() method.
this.state / this.setState
A class component's state object and the method that merges changes into it and schedules a render.
Lifecycle method
A method React calls at a certain moment: after mounting, after updating, before unmounting.
componentDidMount
Runs once after the component first appears in the DOM.
componentDidUpdate
Runs after every update, with the previous props and state, so you can compare.
componentWillUnmount
Runs just before the component is removed: clean up timers and subscriptions here.
PureComponent
A class base that skips rendering when props and state are shallowly equal.
Error boundary
A class component that catches render errors in its children and shows a fallback.
Binding
Fixing what this refers to inside a method, needed when the method is passed as a handler.

Step by step

01Read a class component

Find four things: where state starts (constructor or state =), what render returns, which methods change state, and which lifecycle methods talk to the outside world.

Counter.jsx (class)whole filejsx
class Counter extends Component {
  state = { count: 0, label: "Clicks" };          // initial state

  increment = () => {                               // arrow field keeps `this`
    this.setState((prev) => ({ count: prev.count + 1 })); // merges: label stays
  };

  componentDidUpdate(prevProps, prevState) {
    if (prevState.count !== this.state.count) document.title = "Count " + this.state.count;
  }

  render() {
    return <button onClick={this.increment}>{this.state.label}: {this.state.count}</button>;
  }
}

02Know the lifecycle order

The live example logs every method. Match each line to this diagram.

Know the lifecycle orderdiagram
Rendering diagram…

03Map each method to hooks

Use the table in the code section. The big idea: lifecycle methods split one job across several methods; an effect keeps one job in one place.

04Rewrite the same component with hooks

State fields become useState calls, the componentDidUpdate comparison becomes a dependency array.

Counter.jsx (hooks)whole filejsx
function Counter() {
  const [count, setCount] = useState(0);
  const label = "Clicks";

  useEffect(() => {
    document.title = "Count " + count;
  }, [count]);

  return <button onClick={() => setCount((c) => c + 1)}>{label}: {count}</button>;
}

05Migrate safely

1) Write tests for what the user sees. 2) Convert state and handlers. 3) Merge related lifecycle methods into effects with cleanups. 4) Replace instance fields with refs. 5) Run the tests and StrictMode. Keep error boundaries as classes.

Try it yourself

  1. 1

    Return false from shouldComponentUpdate

    In the lifecycle example, make shouldComponentUpdate return false. Predict the log after the click and what the button shows. (After the click only getDerivedStateFromProps, shouldComponentUpdate and the setState callback log: there's no render and no componentDidUpdate, and the button still says 0 likes. The callback shows likes = 1: the state changed, but React skipped rendering it.)

  2. 2

    Lose this

    In the setState example, change addTwiceRight = () => { to a normal method addTwiceRight() {. Click the button in the preview and read the console: this is undefined inside the handler. Fix it with onClick={() => this.addTwiceRight()} or by binding in a constructor.

  3. 3

    Replace gDSFP with a key

    Delete getDerivedStateFromProps and the userId field from EditName, and render <EditName key={users[i].id} user={users[i]} />. The output stays the same: a new key mounts a fresh component with fresh state.

Code & diagrams

Every lifecycle method, logged New tab

Mount, one update and unmount. `setState` merged `likes` and kept `note`; its callback ran after `componentDidUpdate`, once the update was committed.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

constructor
getDerivedStateFromProps Asha
render
componentDidMount
--- like ---
getDerivedStateFromProps Asha
shouldComponentUpdate
render
getSnapshotBeforeUpdate
componentDidUpdate likes 0 -> 1 snapshot
setState callback: likes = 1 note = hi
--- remove ---
componentWillUnmount
setState: merging, batching, updaters and PureComponent New tab

`name` survives every update because `setState` merges. The object version adds 1 (both calls read the same `this.state`); the updater version adds 2. `PureLabel` renders once: its `text` prop never changed, so the shallow check skips it, while plain `Label` re-renders with its parent.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

Form render: {"name":"Asha","count":0}
  Label render
  PureLabel render
right after setState, this.state.count = 0
Form render: {"name":"Asha","count":1}
  Label render
Form render: {"name":"Asha","count":3}
  Label render
getDerivedStateFromProps: reset a draft when the user changes New tab

The script types like a user would, then switches user. The modern replacement is simpler: `<EditName key={user.id} user={user} />` with plain state, so React remounts it for each user.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

draft after typing: Asha K
draft after switching user: Ravi
An error boundary (still a class) New tab

React logs the caught error with the component name, then the boundary's `componentDidCatch` runs. Resetting the boundary's state renders the children again; this time they succeed.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

Error: price service down

The above error occurred in the <Price> component.

React will try to recreate this component tree from scratch using the error boundary you provided, ErrorBoundary.

componentDidCatch: price service down
shown: Something went wrong: price service down
shown: Price: 499
Migration: three lifecycle methods become one effect New tab

Same behaviour, a third of the code, and no forgotten `prevProps` check. On the update the class lines come first because `componentDidUpdate` runs in the layout step, while `useEffect` runs in the later passive step (Topic 26.1).

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

class connect general
hooks connect general
--- switch room ---
class disconnect general
class connect travel
hooks disconnect general
hooks connect travel
--- close ---
class disconnect travel
hooks disconnect travel
Class → hooks mapping tablemarkdown
| Class | Hooks equivalent | Notes |
|---|---|---|
| `constructor`, `state = {...}` | `useState` (one per value) or `useReducer` | Lazy init: `useState(() => expensive())` |
| `this.setState(partial)` (merges) | setter (replaces) | Spread to merge: `setForm((f) => ({ ...f, name }))` |
| `this.setState(updater)` | `setX((prev) => ...)` | Same batching rules |
| `this.setState(x, callback)` | `useEffect` on the new value, or do the work in the handler | |
| `componentDidMount` | `useEffect(fn, [])` | Plus a cleanup if it starts something |
| `componentDidUpdate(prevProps)` | `useEffect(fn, [deps])` | The deps replace the `prev !== this` checks |
| `componentWillUnmount` | the cleanup returned by the effect | |
| `shouldComponentUpdate`, `PureComponent` | `memo(Component)` | Or the React Compiler (Topic 16.5) |
| `static getDerivedStateFromProps` | compute during render, or reset with `key` | Topics 6.2, 6.4 |
| `getSnapshotBeforeUpdate` | `useLayoutEffect` + a ref | Read the DOM before paint |
| instance fields (`this.timer`) | `useRef` | Changing them doesn't re-render |
| `this.forceUpdate()` | a state setter, or `useSyncExternalStore` for outside data | Topic 10.3 |
| `contextType` / `<Consumer>` | `useContext` or `use(Context)` | Topic 9.2 |
| `getDerivedStateFromError`, `componentDidCatch` | **no hook**: keep a class boundary | Or `react-error-boundary` |
| `UNSAFE_componentWillMount/ReceiveProps/Update` | remove: move work to effects or render | Unsafe with concurrent rendering |

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Mutate this.state directly

In the lifecycle example, change like to this.state.likes++; console.log("likes is", this.state.likes); with no setState.

terminal
$ Preview and console
── what you'll see ──
The console counts up (likes is 1, likes is 2…) but the button keeps showing "Asha: 0 likes". No warning.

Myth vs fact

Myth

Class components are deprecated.

Fact

They are still fully supported in React 19. New code uses functions and hooks, and some new features (hooks themselves, Server Components) are function-only, but classes keep working.

Myth

componentDidMount is the same as useEffect(fn, []).

Fact

Close, but componentDidMount runs in the layout step before paint, like useLayoutEffect; useEffect runs after paint. For most work useEffect is the right replacement anyway.

Myth

setState replaces the whole state.

Fact

Class setState merges top-level keys. It's the hooks setter that replaces the value.

Myth

You must rewrite all classes before using hooks.

Fact

Classes and function components mix in one tree. Migrate when you touch a component, with tests.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    componentDidUpdate running in the layout step means a setState inside it re-renders synchronously before paint, which is a common source of layout jank in old code; the hooks migration usually moves it to useEffect or removes it entirely.

  • ▸

    The UNSAFE_ methods exist because the render phase can be restarted (Topic 26.1): componentWillMount can run several times for one mount, so side effects there duplicate. StrictMode warns about them.

  • ▸

    React 19 removed string refs and contextTypes/childContextTypes (legacy context), and ignores propTypes and defaultProps on function components. Old class code that relies on string refs (ref="input") must move to createRef or callback refs during an upgrade (Topic 23.6).

  • ▸

    React Native, older libraries and many enterprise apps are full of classes. Being able to read this.props.children, HOCs that wrap classes and componentDidUpdate comparisons quickly is a real job skill, not just an interview topic.

Remember this

  1. 1

    A class component is a JavaScript class (see /javascript, classes) that extends Component from React and has a render() method returning JSX. Props arrive as this.props. State is one object, this.state, set in the constructor (after calling super(props)) or with a class field state = { ... }. Methods that are passed as event handlers must keep the right this: write them as arrow-function class fields (handleClick = () => { ... }) or bind them in the constructor, otherwise this is undefined when React calls them.

  2. 2

    this.setState(partial) merges the object you pass into the existing state (only top-level keys), unlike useState's setter, which replaces the value. Updates are batched exactly like hooks updates, so this.state doesn't change until the next render: calling this.setState({ count: this.state.count + 1 }) twice adds one. The fix is the same as with hooks, an updater function: this.setState((prev) => ({ count: prev.count + 1 })). A second argument is a callback that runs after the update is committed, the class equivalent of reacting to a change in an effect.

  3. 3

    Lifecycle methods, in order. Mounting: constructor → static getDerivedStateFromProps → render → (DOM updated) → componentDidMount. Updating: getDerivedStateFromProps → shouldComponentUpdate → render → getSnapshotBeforeUpdate (reads the DOM just before it changes, e.g. scroll position) → componentDidUpdate(prevProps, prevState, snapshot). Unmounting: componentWillUnmount. The first group is the render phase and must be pure; getSnapshotBeforeUpdate runs in the commit's before-mutation step; componentDidMount/Update run in the layout step, at the same time as useLayoutEffect (Topic 26.1).

  4. 4

    Performance controls. shouldComponentUpdate(nextProps, nextState) returns false to skip a render. **PureComponent** implements it for you with a shallow comparison of props and state, the class version of memo (Topic 16.3). **static getDerivedStateFromProps(props, state)** returns an object to merge into state before every render (or null); its only good use is resetting some state when a prop like an id changes, and even that is usually better done with a key (Topic 6.4). The old componentWillMount, componentWillReceiveProps and componentWillUpdate are unsafe in concurrent rendering and are only available with an UNSAFE_ prefix.

  5. 5

    Error boundaries (Topic 14.5) are the one thing hooks can't do. A class with static getDerivedStateFromError(error) (render phase: return state that switches to a fallback UI) and/or componentDidCatch(error, info) (commit phase: log or report it) catches errors thrown while rendering its children, in their lifecycle methods and constructors. It does not catch errors in event handlers, async code or itself. Most teams write one boundary class once, or use the react-error-boundary package, and never write another class.

  6. 6

    Migrating to hooks is done one component at a time; classes and function components mix freely in one tree. The mapping: this.state → one useState per independent value (or useReducer); componentDidMount + componentDidUpdate + componentWillUnmount doing one job → one useEffect with the right dependencies and a cleanup; instance fields like this.timer → useRef; shouldComponentUpdate/PureComponent → memo; getDerivedStateFromProps → compute during render or reset with key; getSnapshotBeforeUpdate → useLayoutEffect with a ref; this.setState(x, callback) → an effect that reacts to the new value, or do the work in the handler. Cover the component with behaviour tests first (Phase 17) so you know the migration changed nothing visible.

Explain it without notes

01

How does this.setState differ from a useState setter?

02

List the lifecycle methods in order for mount, update and unmount, and say which phase each runs in.

03

How do you migrate a class that subscribes in componentDidMount, re-subscribes in componentDidUpdate and unsubscribes in componentWillUnmount?

04

Why can't error boundaries be written with hooks, and what does each boundary method do?

Practice

01

Convert this class to a function component with hooks: a Toggle with state = { on: false }, a flip = () => this.setState((s) => ({ on: !s.on })) method, and componentDidUpdate that logs on is now true/false when on changes.

02

Write a class Ticker that counts up every second from mount and stops on unmount.

Trade-offs

  • ↔

    Migrating a working class costs time and risk; migrate when you are changing the component anyway, or when it blocks using a hook-based library.

  • ↔

    Classes group code by *when* it runs (lifecycle); hooks group it by *what* it does (one effect per concern), which is easier to reuse through custom hooks.

Done when you can

  • I can read a class component and point out its state, handlers and lifecycles.

  • I can explain setState merging, batching, updaters and the callback.

  • I can list the lifecycle methods in order and map each to hooks.

  • I can write an error boundary class.

  • I can migrate a class to hooks without changing its behaviour.