cf.completefrontend
Open lab My progress
THE REACT FIELD GUIDE

Class components and older patterns

Read and maintain older React code: class components, setState, lifecycle methods mapped to Effects, PureComponent, HOCs, render props, and React 19's removals.

By the end, you can
  • 01
    Read a class componentFollow this.props, this.state, and render, say how setState merges and queues, and fix a handler that loses this.
  • 02
    Map classes to HooksMatch lifecycle methods to one Effect, PureComponent to memo, and HOCs and render props to custom Hooks, and know why error boundaries stay classes.
  • 03
    Migrate and upgrade older codeTurn a small class into a function step by step, and spot what React 19 removed: string refs, legacy context, and defaultProps and propTypes on functions.

A cart button written as a class

Hooks arrived in React 16.8, in February 2019. Before that, a component that needed state or had to do something when it appeared was a JavaScript class. Plenty of apps, libraries, and answers online still use classes, and React 19 still runs them. Here is a cart button written that way. Click both buttons.

A cart button as a class componentPop out in the code editor (opens in a new tab)JSX
import { Component } from "react"; class CartButton extends Component {  state = { count: 0 };   render() {    return (      <button onClick={() => this.setState({ count: this.state.count + 1 })}>        {this.props.label}: {this.state.count}      </button>    );  }} export default function App() {  return (    <>      <CartButton label="Tea" />      <CartButton label="Cake" />    </>  );}
ResultLoading…

Each button keeps its own count. Line 3 declares a class that extends React's Component; that makes it a class component. Line 4 is its state, written as a class field: one object, { count: 0 }. Lines 6 to 12 are the render method, which returns the JSX, like a function component's return. Line 9 reads the label prop from this.props and the count from this.state, and line 8 asks for a new count with this.setState. App, lines 15 to 22, is a function component, and it uses the class like any other component: classes and functions mix freely.

A card reading <CartButton label="Tea" /> on the left. An arrow leads to a cart labelled this, holding two cards, props and state. Under it: this.props is label Tea, this.state is count 0. An arrow labelled render() leads to a card, Tea: 0, the button.<CartButton label="Tea" />where App uses itpropsstatethisthis.props = { label: "Tea" }this.state = { count: 0 }render()Tea: 0
For <CartButton label="Tea" />, React makes one object from the class, this, with the props and the state on it, and calls this.render() to get the button. The Cake button gets an object of its own.

Where does this come from? For each <CartButton /> on the page, React creates an object from the class, much as new CartButton(props) would. That object lives as long as the button is on the screen. React keeps the props and the state on it, and calls its render each time it needs the JSX. Here is the same button as a function:

The same cart button as a functionPop out in the code editor (opens in a new tab)JSX
import { useState } from "react"; function CartButton({ label }) {  const [count, setCount] = useState(0);   return (    <button onClick={() => setCount(count + 1)}>      {label}: {count}    </button>  );} export default function App() {  return (    <>      <CartButton label="Tea" />      <CartButton label="Cake" />    </>  );}
ResultLoading…

Line 3 takes label from the props parameter, line 4 replaces the state field and setState with useState, and line 7 calls setCount. There's no this anywhere. How to read the table: each row is one idea, written in a class, then in a function, then what it is.

The same ideas in a class and a function
In a classIn a functionWhat it is
class CartButton extends Componentfunction CartButton({ label })The component itself.
this.props.labellabel, from the props parameterThe props React passes in.
state = { count: 0 }const [count, setCount] = useState(0)The component's memory between renders.
this.setState({ count: 1 })setCount(1)A request to render again with new state.
render() { return … }The function's own return …The JSX to show.
Real-life analogyDriving an older car

If you learned to drive in a new car, an older one feels strange at first. You wind the windows down with a handle, and the switches are in different places. But it drives on the same roads by the same rules, and once you know where each control is, you can drive it, and repair it.

In real life: The same roads and the same rules
In React: The same components, props, JSX, and page
In real life: A handle you turn to wind the window down
In React: this.setState where you'd call a set function
In real life: Switches in places you don't expect
In React: Lifecycle methods where you'd write an Effect
In real life: Learn where they are, and you can drive it and fix it
In React: Learn to read classes, and you can maintain and migrate older code
The main idea

A class component is a JavaScript class that extends Component and has a render method. React makes one object from it for each place it's used, with this.props and this.state on it. React 19 still runs classes. New code uses functions and Hooks; only an error boundary still needs a class.

this.state, and a setState that merges

A class has one state object. this.setState takes an object with the keys you want to change and merges it into the state: the keys you don't mention keep their values. Press Rename to Ravi:

setState keeps the keys you leave outPop out in the code editor (opens in a new tab)JSX
import { Component } from "react"; export default class App extends Component {  state = { name: "Asha", email: "asha@example.com" };   render() {    return (      <>        <button onClick={() => this.setState({ name: "Ravi" })}>Rename to Ravi</button>        <p>Name: {this.state.name}</p>        <p>Email: {this.state.email}</p>      </>    );  }}
ResultLoading…

Line 4 starts the state with two keys. Line 9 passes only name. After the click, the name is Ravi and the email is still asha@example.com. A useState set function doesn't merge. It replaces the value with whatever you give it. Press the same button here:

A set function replaces the whole object (a mistake on purpose)Pop out in the code editor (opens in a new tab)JSX
import { useState } from "react"; export default function App() {  const [profile, setProfile] = useState({ name: "Asha", email: "asha@example.com" });   return (    <>      <button onClick={() => setProfile({ name: "Ravi" })}>Rename to Ravi</button>      <p>Name: {profile.name}</p>      <p>Email: {profile.email}</p>    </>  );}
ResultLoading…

Line 8 gives setProfile an object with only name, so that object is the new state, and the email line goes blank. With useState, you copy the old object first, { ...profile, name: "Ravi" }, as the Updating objects in state lesson shows. When you move a class to a function, every setState with an object needs this change, or a useState for each key.

Both start with name Asha and email asha@example.com. Left, after this.setState({ name: "Ravi" }), a cart labelled state holds Ravi and asha@…: email kept. Right, after setProfile({ name: "Ravi" }), a cart labelled state holds Ravi and a crossed-out asha@…: email gone.this.setState({ name: "Ravi" })Raviasha@…stateemail keptsetProfile({ name: "Ravi" })Raviasha@…stateemail gone
Both start with name: "Asha" and an email. setState merges { name } into the state, so the email stays. A useState set function replaces the whole object, and the new one has no email.

The merge only goes one level deep. this.setState({ address: { city: "Delhi" } }) replaces the whole address object, street and all. Try the three ways of writing the rename:

Playground: setState merges, a set function replaces
What Rename to Ravi runsJavaScript
this.setState({ name: "Ravi" });
The pageclass
state: {"name": "Asha", "email": "asha@example.com"}
Try it yourself
Class: setState

setState merges the object into the state: name changes, and email stays as it was.

Each way runs a real component on this page: a class for setState, a function with useState for the others. Both start with the state shown. Press Rename to Ravi and read the state under the button. Choosing a way, or Reset, starts it over.

Like a set function, setState doesn't change this.state right away. It asks React for a render, and this.state keeps its old value until then:

Two setState calls, with objects and with updatersPop out in the code editor (opens in a new tab)JSX
import { Component } from "react"; export default class App extends Component {  state = { count: 0 };   addTwoObjects = () => {    this.setState({ count: this.state.count + 1 });    this.setState({ count: this.state.count + 1 });  };   addTwoUpdaters = () => {    this.setState((state) => ({ count: state.count + 1 }));    this.setState((state) => ({ count: state.count + 1 }), () => {      console.log("Count is now", this.state.count);    });  };   render() {    return (      <>        <p>Items in cart: {this.state.count}</p>        <button onClick={this.addTwoObjects}>Add 2 with objects</button>        <button onClick={this.addTwoUpdaters}>Add 2 with updaters</button>      </>    );  }}
ResultLoading…

Press Add 2 with objects: the count goes up by 1. Lines 7 and 8 both read this.state.count while it is still 0, so both ask for { count: 1 }. Press Add 2 with updaters: it goes up by 2. Lines 12 and 13 pass an updater function, which React calls with the state after the updates queued before it, as in Queueing a series of state updates. Line 13 also passes a second argument, a function React calls after the update reaches the screen, so line 14 logs the new count: the console shows Count is now 3.

Why this goes missing in a click handler

Class components bring a JavaScript problem with them: this depends on how a function is called. Click Add tea here:

A method passed as a click handler (an error on purpose)Pop out in the code editor (opens in a new tab)JSX
import { Component } from "react"; export default class App extends Component {  state = { count: 0 };   addTea() {    this.setState({ count: this.state.count + 1 });  }   render() {    return (      <>        <p>Items in cart: {this.state.count}</p>        <button onClick={this.addTea}>Add tea</button>      </>    );  }}
ResultLoading…

The count doesn't change, and the error appears: TypeError: Cannot read properties of undefined (reading 'setState'). Line 14 passes this.addTea, which is the function alone, without the object it belongs to. When you click, React calls that function on its own. Code inside a class always runs in strict mode, where a function called on its own gets this as undefined, so line 7's this.setState fails. The Losing this lesson covers why. Older code fixes it in one of two ways:

Two ways older code keeps thisPop out in the code editor (opens in a new tab)JSX
import { Component } from "react"; export default class App extends Component {  constructor(props) {    super(props);    this.state = { count: 0 };    this.addTea = this.addTea.bind(this);  }   addTea() {    this.setState({ count: this.state.count + 1 });  }   addCake = () => {    this.setState({ count: this.state.count + 1 });  };   render() {    return (      <>        <p>Items in cart: {this.state.count}</p>        <button onClick={this.addTea}>Add tea</button>        <button onClick={this.addCake}>Add cake</button>      </>    );  }}
ResultLoading…
  • Lines 4 to 8 are a constructor. Line 5, super(props), runs Component's constructor first, as every subclass constructor must. Line 6 sets the state, and line 7 replaces addTea on this object with a copy whose this is fixed, made by bind.
  • Lines 14 to 16 make addCake a class field holding an arrow function. An arrow function has no this of its own, so it uses the object's. Code from the last years of classes mostly does this.

Both buttons work. A third way is an arrow in the JSX, onClick={() => this.addTea()}, which calls the method on the object. Function components don't have this problem at all: there's no this.

Lifecycle methods, and the Effect that replaced them

A class reacts to moments in its life with lifecycle methods: methods React calls when the component appears (mounts), after it updates, and before it goes (unmounts). Here is a chat room that connects to the chosen room. The chat.js tab is a pretend server that logs each connect and disconnect, as the Synchronizing with effects lesson's did.

A chat room as a class, with three lifecycle methodsPop out in the code editor (opens in a new tab)JSX
import { Component, useState } from "react";import { createConnection } from "./chat.js"; class ChatRoom extends Component {  componentDidMount() {    this.connection = createConnection(this.props.roomId);    this.connection.connect();  }   componentDidUpdate(prevProps) {    if (prevProps.roomId !== this.props.roomId) {      this.connection.disconnect();      this.connection = createConnection(this.props.roomId);      this.connection.connect();    }  }   componentWillUnmount() {    this.connection.disconnect();  }   render() {    return <h2>Welcome to {this.props.roomId}</h2>;  }} export default function App() {  const [roomId, setRoomId] = useState("general");  const [show, setShow] = useState(true);   return (    <>      <button onClick={() => setRoomId(roomId === "general" ? "travel" : "general")}>Switch room</button>      <button onClick={() => setShow(!show)}>{show ? "Close chat" : "Open chat"}</button>      {show && <ChatRoom roomId={roomId} />}    </>  );}
ResultLoading…
  • componentDidMount, lines 5 to 8, runs once the room is on the screen. It connects, and keeps the connection on this.connection for later.
  • componentDidUpdate, lines 10 to 16, runs after every later render that reached the screen. It gets the old props, so line 11 compares the old roomId with the new one, and reconnects only when it changed.
  • componentWillUnmount, lines 18 to 20, runs just before the room is removed, and disconnects.

The console starts with three lines: connected, disconnected, connected. In development, StrictMode runs componentDidMount, then componentWillUnmount, then componentDidMount again, to check that the two match, the same check it does for Effects. Press Switch room: the room disconnects from general and connects to travel. Press Close chat: it disconnects from travel. Open connections end at 0.

Three methods share one job: keep the connection matching roomId. Forget one, and the job breaks. This version has no componentDidUpdate:

The chat room without componentDidUpdate (a bug on purpose)Pop out in the code editor (opens in a new tab)JSX
import { Component, useState } from "react";import { createConnection } from "./chat.js"; class ChatRoom extends Component {  componentDidMount() {    this.connection = createConnection(this.props.roomId);    this.connection.connect();  }   componentWillUnmount() {    this.connection.disconnect();  }   render() {    return <h2>Welcome to {this.props.roomId}</h2>;  }} export default function App() {  const [roomId, setRoomId] = useState("general");  const [show, setShow] = useState(true);   return (    <>      <button onClick={() => setRoomId(roomId === "general" ? "travel" : "general")}>Switch room</button>      <button onClick={() => setShow(!show)}>{show ? "Close chat" : "Open chat"}</button>      {show && <ChatRoom roomId={roomId} />}    </>  );}
ResultLoading…

Press Switch room: the heading says travel, but the console is silent, because nothing reconnects. Press Close chat: it logs Disconnected from general, the room it really joined. This was one of the most common class bugs. Here is the room with an Effect:

The same chat room with one EffectPop out in the code editor (opens in a new tab)JSX
import { useEffect, useState } from "react";import { createConnection } from "./chat.js"; function ChatRoom({ roomId }) {  useEffect(() => {    const connection = createConnection(roomId);    connection.connect();    return () => connection.disconnect();  }, [roomId]);   return <h2>Welcome to {roomId}</h2>;} export default function App() {  const [roomId, setRoomId] = useState("general");  const [show, setShow] = useState(true);   return (    <>      <button onClick={() => setRoomId(roomId === "general" ? "travel" : "general")}>Switch room</button>      <button onClick={() => setShow(!show)}>{show ? "Close chat" : "Open chat"}</button>      {show && <ChatRoom roomId={roomId} />}    </>  );}
ResultLoading…

Lines 5 to 9 are the whole job. The setup, lines 6 and 7, connects. The cleanup on line 8 disconnects. The dependency list on line 9 says the Effect reads roomId, so when it changes, React runs the cleanup for the old room and the setup for the new one. Click the same buttons: the console shows the same lines as the class.

Three columns: it appears, roomId changes, it goes away. Top row, the class methods: componentDidMount, componentDidUpdate, componentWillUnmount. An arrow leads down from each to the part of one Effect that does the same job: setup; cleanup, then setup; cleanup.It appearscomponentDidMountsetuproomId changescomponentDidUpdatecleanup, then setupIt goes awaycomponentWillUnmountcleanupone useEffect, with [roomId]
Top: the class needs a method for each moment. Bottom: one Effect with [roomId] covers all three, because React runs its cleanup and setup again whenever roomId changes.

So don't translate method by method. Ask what the methods keep in step, and write one Effect for each such thing. How to read the table: each class method, when React calls it, and what you write with Hooks.

Lifecycle methods and their Hook versions
Class methodWhen React calls itWith Hooks
constructor(props)Once, before the first render.useState(initial) for state; useRef for values like this.connection.
render()On every render.The function body that returns JSX.
componentDidMount()Once, after the component is first on the screen.An Effect's setup code.
componentDidUpdate(prevProps, prevState)After every later render that reached the screen.The same Effect, run again when a dependency changes. React compares them for you.
componentWillUnmount()Just before the component is removed.The cleanup function the Effect returns.
shouldComponentUpdate(nextProps, nextState)Before a render; returning false skips it.memo, which skips a render when the props are the same.
static getDerivedStateFromProps(props, state)Before every render.Work the value out during render, or reset the state with a key.
getSnapshotBeforeUpdate()Just before React changes the DOM.No Hook yet. It's rare: keeping a scroll position.
static getDerivedStateFromError() and componentDidCatch()When a component inside throws.No Hook yet: an error boundary is still a class.

Methods named componentWillMount, componentWillReceiveProps, and componentWillUpdate, with or without an UNSAFE_ prefix, are the oldest of all. React 19 still calls them, with warnings: the plain names "have been renamed", and the UNSAFE_ names are flagged in StrictMode. Move their work to the methods above, or to Effects.

Error boundaries are still classes

One job has no Hook in React 19: catching an error thrown while the components inside render. That takes a class with static getDerivedStateFromError, which returns the state that shows a fallback, and usually componentDidCatch, which reports the error. The Error boundaries lesson builds one. Write it once and reuse it, or use the react-error-boundary package.

PureComponent and memo

A class re-renders whenever its parent does, like a function component. PureComponent changes that: before each render it compares the new props and state with the old ones, one by one with Object.is, and skips the render when they're all the same. Type in the box below; the small numbers count each component's renders (the RenderCount.jsx tab is the helper from the Context lesson).

Component, PureComponent, and memoPop out in the code editor (opens in a new tab)JSX
import { Component, PureComponent, memo, useState } from "react";import RenderCount from "./RenderCount.jsx"; class PlainTotal extends Component {  render() {    return <p>Component: ${this.props.total} <RenderCount /></p>;  }} class PureTotal extends PureComponent {  render() {    return <p>PureComponent: ${this.props.total} <RenderCount /></p>;  }} const MemoTotal = memo(function MemoTotal({ total }) {  return <p>memo: ${total} <RenderCount /></p>;}); export default function App() {  const [note, setNote] = useState("");  const total = 7;   return (    <>      <input value={note} onChange={(event) => setNote(event.target.value)} placeholder="A note for the shop" />      <PlainTotal total={total} />      <PureTotal total={total} />      <MemoTotal total={total} />    </>  );}
ResultLoading…

Each key you type changes note in App, so App renders again. PlainTotal, lines 4 to 8, is a plain Component, and its count goes up with every key. PureTotal, lines 10 to 14, extends PureComponent: its total prop is still 7, so it stays at 1. MemoTotal, lines 16 to 18, is a function wrapped in memo, which does the same for a function's props, and stays at 1 too.

PureComponent is a built-in shouldComponentUpdate, the method a class writes to decide for itself: return false to skip. Both fail in the same ways as memo: a new object or function in a prop on every render makes the comparison fail every time. The React Compiler only compiles functions and Hooks, so it adds no memoizing to a class.

Higher-order components and render props

Before Hooks, two components that needed the same stateful logic had no simple way to share it. Two patterns grew up for it, and you'll meet both in older code. Here two order cards share a toggle that shows their items. First, a higher-order component (HOC): a function that takes a component and returns a new one that wraps it.

A toggle shared by a higher-order componentPop out in the code editor (opens in a new tab)JSX
import { Component } from "react"; function withToggle(WrappedComponent) {  return class WithToggle extends Component {    state = { open: false };    toggle = () => this.setState({ open: !this.state.open });     render() {      return <WrappedComponent {...this.props} open={this.state.open} onToggle={this.toggle} />;    }  };} function OrderCard({ order, open, onToggle }) {  return (    <section>      <h3>Order {order.id}</h3>      <button onClick={onToggle}>{open ? "Hide" : "Show"} items</button>      {open && <p>{order.items.join(", ")}</p>}    </section>  );} const ToggleOrderCard = withToggle(OrderCard); const orders = [  { id: 1042, items: ["Tea", "Cake"] },  { id: 1043, items: ["Lemonade"] },]; export default function App() {  return orders.map((order) => <ToggleOrderCard key={order.id} order={order} />);}
ResultLoading…

withToggle, lines 3 to 12, returns a class that holds open in its state. Its render, line 9, renders the wrapped component with every prop it got, spread with {...this.props}, plus open and onToggle. OrderCard, lines 14 to 22, just uses those props. Line 24 makes the wrapped card, and App uses ToggleOrderCard. Press Show items on each card: each has its own state.

Their names start with with. In older apps you'll see react-redux's connect(…)(Component) and React Router 5's withRouter. Reading them is harder than it looks: OrderCard's open comes from a file that never mentions it, each wrapper adds a component to the tree, and two HOCs that both pass a prop named open silently overwrite each other.

The other pattern is a render prop: a prop holding a function that returns JSX. The component with the logic calls it with its values:

The same toggle shared by a render propPop out in the code editor (opens in a new tab)JSX
import { Component } from "react"; class Toggle extends Component {  state = { open: false };  toggle = () => this.setState({ open: !this.state.open });   render() {    return this.props.children({ open: this.state.open, toggle: this.toggle });  }} function OrderCard({ order }) {  return (    <Toggle>      {({ open, toggle }) => (        <section>          <h3>Order {order.id}</h3>          <button onClick={toggle}>{open ? "Hide" : "Show"} items</button>          {open && <p>{order.items.join(", ")}</p>}        </section>      )}    </Toggle>  );} const orders = [  { id: 1042, items: ["Tea", "Cake"] },  { id: 1043, items: ["Lemonade"] },]; export default function App() {  return orders.map((order) => <OrderCard key={order.id} order={order} />);}
ResultLoading…

Toggle's render, line 8, calls this.props.children as a function, with open and toggle. OrderCard passes that function as Toggle's children, lines 15 to 21, and returns the card from it. The values now come from a visible place, but every shared value adds a level of brackets. React's own context had a render prop before useContext, and it still works:

React's own render prop: a context ConsumerJSX
<ThemeContext.Consumer>
  {(theme) => <p className={theme}>Order 1042</p>}
</ThemeContext.Consumer>

Hooks replaced both. A custom Hook keeps the logic, and the component calls it:

The same toggle as a custom HookPop out in the code editor (opens in a new tab)JSX
import { useState } from "react"; function useToggle() {  const [open, setOpen] = useState(false);  return [open, () => setOpen(!open)];} function OrderCard({ order }) {  const [open, toggle] = useToggle();   return (    <section>      <h3>Order {order.id}</h3>      <button onClick={toggle}>{open ? "Hide" : "Show"} items</button>      {open && <p>{order.items.join(", ")}</p>}    </section>  );} const orders = [  { id: 1042, items: ["Tea", "Cake"] },  { id: 1043, items: ["Lemonade"] },]; export default function App() {  return orders.map((order) => <OrderCard key={order.id} order={order} />);}
ResultLoading…

useToggle, lines 3 to 6, holds the same state. Line 9 calls it, and the values arrive on a line you can read. No wrapper component, no function as a child, and each card still has its own state. A function prop that only draws part of the page, like renderItem in Component composition, is still a common pattern; it's sharing state through one that Hooks replaced.

Three component trees. Higher-order component: WithToggle, with an arrow down to OrderCard. Render prop: OrderCard, an arrow to Toggle, an arrow to a function card, open and toggle. Custom Hook: OrderCard alone, with useToggle() written inside it.Higher-order componentWithToggleOrderCardRender propOrderCardToggle({ open, toggle })Custom HookOrderCarduseToggle()called inside
The same toggle three ways. A higher-order component wraps OrderCard in another component. A render prop puts Toggle and a function between it and its JSX. A custom Hook adds nothing: OrderCard calls useToggle() itself.

How to read the table: each pattern, how OrderCard gets open and toggle, and what that costs.

Three ways to share the toggle
PatternHow OrderCard gets themWhat it costs
Higher-order componentwithToggle wraps it in another component, which passes open and onToggle as props.An extra component per wrapper, and props whose source isn't in the file.
Render propToggle calls the function you pass as its children, with { open, toggle }.Nesting: each shared value adds another level of brackets.
Custom HookIt calls useToggle() itself and gets [open, toggle].Nothing extra in the tree; the logic is a function the component calls.

Turn a class into a function, step by step

You don't have to rewrite every class: classes and functions work together, and React keeps running both. When you do change one, go in small steps. Here is a note box for an order that logs the draft each time it changes. Type a few letters and watch the console:

import { Component } from "react"; class NoteBox extends Component {  constructor(props) {    super(props);    this.state = { note: "" };    this.handleChange = this.handleChange.bind(this);  }   componentDidMount() {    console.log(`Draft for ${this.props.orderId}: "${this.state.note}"`);  }   componentDidUpdate(prevProps, prevState) {    if (prevProps.orderId !== this.props.orderId || prevState.note !== this.state.note) {      console.log(`Draft for ${this.props.orderId}: "${this.state.note}"`);    }  }   handleChange(event) {    this.setState({ note: event.target.value });  }   render() {    return (      <label>        Note for order {this.props.orderId}{" "}        <input value={this.state.note} onChange={this.handleChange} />      </label>    );  }} export default function App() {  return <NoteBox orderId={1042} />;}
ResultLoading…

Lines 4 to 8 set the state and bind handleChange. componentDidMount, lines 10 to 12, logs the empty draft, and componentDidUpdate, lines 14 to 18, logs it again when the order or the note changed. Step through the migration; each step changes one kind of thing:

Playground: a class to a function, one step at a time
NoteBox, as a classJSX
import { Component } from "react"; class NoteBox extends Component {  constructor(props) {    super(props);    this.state = { note: "" };    this.handleChange = this.handleChange.bind(this);  }   componentDidMount() {    console.log(`Draft for ${this.props.orderId}: "${this.state.note}"`);  }   componentDidUpdate(prevProps, prevState) {    if (prevProps.orderId !== this.props.orderId || prevState.note !== this.state.note) {      console.log(`Draft for ${this.props.orderId}: "${this.state.note}"`);    }  }   handleChange(event) {    this.setState({ note: event.target.value });  }   render() {    return (      <label>        Note for order {this.props.orderId}{" "}        <input value={this.state.note} onChange={this.handleChange} />      </label>    );  }} export default function App() {  return <NoteBox orderId={1042} />;}
Step 1 of 5
Start: the class

A class with state, a bound method, and two lifecycle methods that log the draft. Every step below changes one kind of thing.

The marked lines are the ones each step changed. Steps 1 to 3 are halfway: they still hold class-only pieces, like this or componentDidMount, so the code doesn't run until step 4. The class above and the function below run as live examples, and log the same lines.

Here is where it ends. Type the same letters, and the console shows the same lines as the class:

NoteBox as a functionPop out in the code editor (opens in a new tab)JSX
import { useEffect, useState } from "react"; function NoteBox({ orderId }) {  const [note, setNote] = useState("");   useEffect(() => {    console.log(`Draft for ${orderId}: "${note}"`);  }, [orderId, note]);   function handleChange(event) {    setNote(event.target.value);  }   return (    <label>      Note for order {orderId}{" "}      <input value={note} onChange={handleChange} />    </label>  );} export default function App() {  return <NoteBox orderId={1042} />;}
ResultLoading…

Line 4 is the state. Lines 6 to 8 are one Effect that runs after the first render, and again when orderId or note changes: exactly what the two lifecycle methods did with their comparisons. Lines 10 to 12 are the handler, with no this to bind. Check a migration the way you just did: the same output, the same logs, then delete the class.

What React 19 removed from older code

React 19 removed APIs that were deprecated years before, and older class code is where you'll find them. Some now do nothing, which is easy to miss. This page shows two headings:

defaultProps on a function does nothing in React 19Pop out in the code editor (opens in a new tab)JSX
function Heading({ text }) {  return <h1>{text}</h1>;}Heading.defaultProps = { text: "Welcome to the tea shop" }; function Greeting({ text = "Welcome to the tea shop" }) {  return <h1>{text}</h1>;} export default function App() {  return (    <>      <Heading />      <Greeting />    </>  );}
ResultLoading…

Only one heading shows text. Line 4 sets defaultProps on the function Heading, and React 19 doesn't read it, so text is undefined and the first <h1> is empty. Line 6 gives the default in the parameter, with destructuring, and the second heading shows it. Class components still read static defaultProps. propTypes are ignored for every component: React no longer checks them or warns, so use TypeScript to check props instead.

Other removals throw. Older classes reached elements with string refs:

A string ref (an error on purpose)Pop out in the code editor (opens in a new tab)JSX
import { Component } from "react"; export default class App extends Component {  focusSearch = () => {    this.refs.search.focus();  };   render() {    return (      <>        <input ref="search" placeholder="Search teas" />        <button onClick={this.focusSearch}>Find a tea</button>      </>    );  }}
ResultLoading…

Line 11's ref="search" makes React 19 throw as soon as it renders: Expected ref to be a function, an object returned by React.createRef(), or undefined/null. Line 5 would have read the input as this.refs.search. A callback ref does the same job:

The same class with a callback refPop out in the code editor (opens in a new tab)JSX
import { Component } from "react"; export default class App extends Component {  focusSearch = () => {    this.search.focus();  };   render() {    return (      <>        <input ref={(node) => (this.search = node)} placeholder="Search teas" />        <button onClick={this.focusSearch}>Find a tea</button>      </>    );  }}
ResultLoading…

Line 11 passes a function. React calls it with the input's node, which it keeps on this.search, and line 5 focuses it. Click Find a tea: the box gets the cursor. Legacy context, the way classes shared values before createContext, is gone too:

Legacy context, and the contextType that replaced itJSX
// Removed in React 19: React ignores these, and warns.
class Price extends Component {
  static contextTypes = { currency: PropTypes.string };
  render() {
    return <p>{this.context.currency} 3</p>;
  }
}

// Still works: one context, read through static contextType.
const CurrencyContext = createContext("$");

class Price extends Component {
  static contextType = CurrencyContext;
  render() {
    return <p>{this.context} 3</p>;
  }
}

The first class reads this.context.currency through static contextTypes; React 19 ignores that and warns, so this.context is empty. The second reads one context with static contextType, which still works. A function uses useContext. How to read the table: older code, what React 19 does with it, and what to write instead.

What React 19 removed
Older codeIn React 19Write instead
Heading.defaultProps = { … } on a functionReact ignores it.Default values in the parameters: ({ text = "…" }). Classes keep static defaultProps.
Heading.propTypes = { … }React ignores it: no checks, no warnings.TypeScript, which checks props while you type.
ref="search" and this.refs.searchAn error when the component renders.A callback ref, createRef(), or useRef in a function.
contextTypes and getChildContextReact ignores them, and warns.createContext with static contextType, or useContext in a function.
ReactDOM.render, ReactDOM.hydrate, unmountComponentAtNodeGone from react-dom.createRoot(el).render(…), hydrateRoot, and root.unmount().
ReactDOM.findDOMNode(this)Gone from react-dom.A ref on the element you need.
React.createFactory(Button)Gone from react.JSX, or createElement(Button, props).

The React team and codemod.com publish codemods, programs that rewrite code for you, for several of these. npx codemod@latest react/19/migration-recipe replaces ReactDOM.render, string refs, and more. Read the React 19 upgrade guide on react.dev before you upgrade an older app.

Reading an older component file

When you open an older file, read it in this order. Each part tells you what the Hook version would be:

  1. The state, in the constructor or the state field: each key is a likely useState.
  2. The lifecycle methods: what do they start, update, and stop together? Each such thing is one Effect.
  3. The methods: event handlers, bound in the constructor or written as arrow fields. They become functions inside the component.
  4. The export line: export default withSomething(MyComponent) means a HOC passes it props you won't find in the file.
  5. render: the JSX, with this.props. and this.state. in front of every value.
Which Hook or pattern replaces it?
  • this.setState({ open: true });
  • state = { count: 0 };
  • componentDidMount() { this.connection.connect(); }
  • componentWillUnmount() { this.connection.disconnect(); }
  • export default withToggle(OrderCard);
  • <Toggle>{({ open, toggle }) => …}</Toggle>
  • static getDerivedStateFromError(error) { return { error }; }
  • componentDidCatch(error, info) { report(error, info); }
Try it yourself
0 of 8 correct

Sort each line from an older class by what you'd write in a function component today.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

Common mix-ups

  • "Class components are deprecated." No. React 19 runs them, and the React team has said it has no plans to remove them. They're not recommended for new code.
  • "useEffect(fn, []) is componentDidMount." Only when the setup reads no props or state. An Effect that reads roomId lists it, and does componentDidUpdate's job too.
  • "A set function merges like setState." No. setProfile({ name }) replaces the whole object. Spread the old one in first.
  • "this.state changes as soon as I call setState." No. It changes on the next render. Use an updater function to build on the newest state.
  • "PureComponent makes rendering pure." No. Rendering should already be pure. PureComponent skips renders when the props and state are shallowly equal.
  • "I must rewrite all classes before upgrading." No. Classes and functions work together. Fix what React 19 removed, and migrate the rest when you change them.

Practice exercises

Each exercise shows its code's result. Read the class from the top: the state, then the methods, then render. Change the code with Edit & run, and the result updates as you type.

Exercise 1 · Warm-upPredict a merge

What does the paragraph say after you click Move to Delhi? Answer first, then click to check.

import { Component } from "react"; export default class App extends Component {  state = { name: "Asha", city: "Pune" };   render() {    return (      <>        <p>          {this.state.name} from {this.state.city}        </p>        <button onClick={() => this.setState({ city: "Delhi" })}>Move to Delhi</button>      </>    );  }}
ResultLoading…

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · Warm-upTwo setState calls in one click

    What does it show after one click on Add two teas? Then make the button really add two.

    import { Component } from "react"; export default class App extends Component {  state = { count: 0 };   addTwoTeas = () => {    this.setState({ count: this.state.count + 1 });    this.setState({ count: this.state.count + 1 });  };   render() {    return (      <>        <p>Teas: {this.state.count}</p>        <button onClick={this.addTwoTeas}>Add two teas</button>      </>    );  }}
    ResultLoading…

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeFix the lost this

      Clicking Remove the first wish throws an error. Fix it. After your fix, how many wishes are left after one click?

      import { Component } from "react"; export default class App extends Component {  state = { wishes: ["Green tea", "Lemon cake", "Mint tea"] };   removeFirst() {    this.setState({ wishes: this.state.wishes.slice(1) });  }   render() {    return (      <>        <p>Wishes: {this.state.wishes.length}</p>        <button onClick={this.removeFirst}>Remove the first wish</button>        <ul>          {this.state.wishes.map((wish) => (            <li key={wish}>{wish}</li>          ))}        </ul>      </>    );  }}
      ResultLoading…

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeAdd the missing lifecycle method

        Switch room changes the heading but never reconnects. Add the method that fixes it. After your fix, what does the console's last line say after Switch room?

        import { Component, useState } from "react";import { createConnection } from "./chat.js"; class ChatRoom extends Component {  componentDidMount() {    this.connection = createConnection(this.props.roomId);    this.connection.connect();  }   componentWillUnmount() {    this.connection.disconnect();  }   render() {    return <h2>Welcome to {this.props.roomId}</h2>;  }} export default function App() {  const [roomId, setRoomId] = useState("general");  const [show, setShow] = useState(true);   return (    <>      <button onClick={() => setRoomId(roomId === "general" ? "travel" : "general")}>Switch room</button>      <button onClick={() => setShow(!show)}>{show ? "Close chat" : "Open chat"}</button>      {show && <ChatRoom roomId={roomId} />}    </>  );}
        ResultLoading…

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeMigrate the quantity picker

          Turn Quantity into a function component with useState. It should work the same. After +, +, and −, what is the quantity?

          import { Component } from "react"; class Quantity extends Component {  constructor(props) {    super(props);    this.state = { quantity: 1 };    this.increase = this.increase.bind(this);    this.decrease = this.decrease.bind(this);  }   increase() {    this.setState({ quantity: this.state.quantity + 1 });  }   decrease() {    this.setState({ quantity: Math.max(1, this.state.quantity - 1) });  }   render() {    return (      <p>        {this.props.name}{" "}        <button onClick={this.decrease}>−</button> {this.state.quantity}{" "}        <button onClick={this.increase}>+</button>      </p>    );  }} export default function App() {  return <Quantity name="Tea" />;}
          ResultLoading…

          Answer, then press Check. Spacing and letter case don’t matter.

            Exercise 6 · ChallengeReplace the render prop with a Hook

            Replace the Toggle class and its render prop with a useToggle Hook. After Show items on order 1043, what does the card show?

            import { Component } from "react"; class Toggle extends Component {  state = { open: false };  toggle = () => this.setState({ open: !this.state.open });   render() {    return this.props.children({ open: this.state.open, toggle: this.toggle });  }} function OrderCard({ order }) {  return (    <Toggle>      {({ open, toggle }) => (        <section>          <h3>Order {order.id}</h3>          <button onClick={toggle}>{open ? "Hide" : "Show"} items</button>          {open && <p>{order.items.join(", ")}</p>}        </section>      )}    </Toggle>  );} const orders = [  { id: 1042, items: ["Tea", "Cake"] },  { id: 1043, items: ["Lemonade"] },]; export default function App() {  return orders.map((order) => <OrderCard key={order.id} order={order} />);}
            ResultLoading…

            Answer, then press Check. Spacing and letter case don’t matter.

              Check your understanding

              For each code question, find this.state first, then follow each setState call.

              The Class components quiz · 8 questionsScore: first tries count
              1. Question 1 of 8After you click Change email once, what does the paragraph show?

                Read the code, then predictPop out in the code editor (opens in a new tab)JSX
                import { Component } from "react";
                
                export default class Profile extends Component {
                  state = { name: "Asha", email: "asha@example.com" };
                
                  render() {
                    return (
                      <>
                        <button onClick={() => this.setState({ email: "asha@tea.shop" })}>Change email</button>
                        <p>{this.state.name}</p>
                      </>
                    );
                  }
                }

                Choose an answer to see the explanation.

              2. Question 2 of 8What does the button show after one click on it?

                Read the code, then predictPop out in the code editor (opens in a new tab)JSX
                import { Component } from "react";
                
                export default class Cart extends Component {
                  state = { count: 0 };
                
                  addThree = () => {
                    this.setState({ count: this.state.count + 1 });
                    this.setState({ count: this.state.count + 1 });
                    this.setState({ count: this.state.count + 1 });
                  };
                
                  render() {
                    return <button onClick={this.addThree}>{this.state.count}</button>;
                  }
                }

                Choose an answer to see the explanation.

              3. Question 3 of 8A class passes onClick={this.addTea}, and addTea is a plain method. Why is this undefined when it runs?

                Choose an answer to see the explanation.

              4. Question 4 of 8A class connects in componentDidMount, reconnects in componentDidUpdate when roomId changes, and disconnects in componentWillUnmount. What replaces all three?

                Choose an answer to see the explanation.

              5. Question 5 of 8Can you write an error boundary as a function component in React 19?

                Choose an answer to see the explanation.

              6. Question 6 of 8What does extending PureComponent do?

                Choose an answer to see the explanation.

              7. Question 7 of 8In React 19, what does <Badge /> show?

                Read the code, then predictPop out in the code editor (opens in a new tab)JSX
                function Badge({ label }) {
                  return <span>{label ?? "nothing"}</span>;
                }
                Badge.defaultProps = { label: "New" };
                
                export default function App() {
                  return <Badge />;
                }

                Choose an answer to see the explanation.

              8. Question 8 of 8What replaced higher-order components and render props for sharing stateful logic?

                Choose an answer to see the explanation.

              Key takeaways

              • A class component extends Component and has a render method. React makes one object per use, with this.props and this.state.
              • this.setState merges an object into the state, one level deep, and this.state changes on the next render. Use updater functions to build on the newest state.
              • A method passed as a handler loses this. Older code binds it in the constructor or writes it as an arrow class field.
              • componentDidMount, componentDidUpdate, and componentWillUnmount split one job; an Effect with its dependencies does it whole.
              • PureComponent is to classes what memo is to functions. Error boundaries are still classes.
              • Higher-order components and render props shared logic before Hooks; custom Hooks do it without extra components.
              • React 19 ignores defaultProps on functions and propTypes, and removed string refs, legacy context, and ReactDOM.render.

              A class keeps its state on this and splits its work across lifecycle methods.
              Read it by asking what each part keeps in step, and that's the Hook version.

              Coming next: Routing. It starts the React ecosystem: how a single-page app maps addresses to screens.

              CompleteFrontend Clear concepts. Working examples.