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.
- 01Read a class componentFollow this.props, this.state, and render, say how setState merges and queues, and fix a handler that loses this.
- 02Map 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.
- 03Migrate 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.
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" /> </> );}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.
<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:
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" /> </> );}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.
| In a class | In a function | What it is |
|---|---|---|
class CartButton extends Component | function CartButton({ label }) | The component itself. |
this.props.label | label, from the props parameter | The 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. |
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.setStatewhere 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
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:
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> </> ); }}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:
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> </> );}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.
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:
this.setState({ name: "Ravi" });classstate: {"name": "Asha", "email": "asha@example.com"}setState merges the object into the state: name changes, and email stays as it was.
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:
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> </> ); }}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:
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> </> ); }}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:
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> </> ); }}- Lines 4 to 8 are a constructor. Line 5,
super(props), runsComponent's constructor first, as every subclass constructor must. Line 6 sets the state, and line 7 replacesaddTeaon this object with a copy whosethisis fixed, made by bind. - Lines 14 to 16 make
addCakea class field holding an arrow function. An arrow function has nothisof 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.
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} />} </> );}componentDidMount, lines 5 to 8, runs once the room is on the screen. It connects, and keeps the connection onthis.connectionfor 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 oldroomIdwith 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:
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} />} </> );}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:
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} />} </> );}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.
[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.
| Class method | When React calls it | With 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.
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).
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} /> </> );}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.
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} />;}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:
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} />;}A class with state, a bound method, and two lifecycle methods that log the draft. Every step below changes one kind of thing.
Here is where it ends. Type the same letters, and the console shows the same lines as the class:
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} />;}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:
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 /> </> );}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:
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> </> ); }}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:
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> </> ); }}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:
// 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.
| Older code | In React 19 | Write instead |
|---|---|---|
Heading.defaultProps = { … } on a function | React 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.search | An error when the component renders. | A callback ref, createRef(), or useRef in a function. |
contextTypes and getChildContext | React ignores them, and warns. | createContext with static contextType, or useContext in a function. |
ReactDOM.render, ReactDOM.hydrate, unmountComponentAtNode | Gone 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:
- The state, in the constructor or the
statefield: each key is a likelyuseState. - The lifecycle methods: what do they start, update, and stop together? Each such thing is one Effect.
- The methods: event handlers, bound in the constructor or written as arrow fields. They become functions inside the component.
- The export line:
export default withSomething(MyComponent)means a HOC passes it props you won't find in the file. - render: the JSX, with
this.props.andthis.state.in front of every value.
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); }
Sort each line from an older class by what you'd write in a function component today.
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
roomIdlists 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.
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> </> ); }}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> </> ); }}setState merges { city: "Delhi" } into the state, so name stays "Asha" and the paragraph says Asha from Delhi.
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> </> ); }}import { Component } from "react"; export default class App extends Component { state = { count: 0 }; addTwoTeas = () => { this.setState((state) => ({ count: state.count + 1 })); this.setState((state) => ({ count: state.count + 1 })); }; render() { return ( <> <p>Teas: {this.state.count}</p> <button onClick={this.addTwoTeas}>Add two teas</button> </> ); }}Before the fix, both calls read this.state.count as 0 and ask for 1, so one click shows 1. With (state) => ({ count: state.count + 1 }), each updater gets the state after the one before, so a click adds 2.
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> </> ); }}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> </> ); }}As an arrow function in a class field, removeFirst uses the object's this, so this.setState works. One click removes Green tea and leaves 2 wishes. Binding it in a constructor would work too.
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} />} </> );}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} />} </> );}componentDidUpdate compares the old roomId with the new one. When they differ, it disconnects from general and connects to travel, so the console's last line is Connected to travel. Open connections: 1.
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" />;}import { useState } from "react"; function Quantity({ name }) { const [quantity, setQuantity] = useState(1); function increase() { setQuantity(quantity + 1); } function decrease() { setQuantity(Math.max(1, quantity - 1)); } return ( <p> {name}{" "} <button onClick={decrease}>−</button> {quantity}{" "} <button onClick={increase}>+</button> </p> );} export default function App() { return <Quantity name="Tea" />;}useState(1) replaces the state, and increase and decrease become plain functions that read quantity, so nothing needs binding. +, +, − goes 1, 2, 3, 2.
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} />);}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} />);}useToggle keeps the state Toggle kept, and OrderCard calls it, so the tree has no Toggle components. Order 1043's items are Lemonade.
Check your understanding
For each code question, find this.state first, then follow each setState call.
Question 1 of 8After you click Change email once, what does the paragraph show?
Read the code, then predictimport { 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.
Question 2 of 8What does the button show after one click on it?
Read the code, then predictimport { 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.
Question 3 of 8A class passes
onClick={this.addTea}, andaddTeais a plain method. Why isthisundefined when it runs?Choose an answer to see the explanation.
Question 4 of 8A class connects in componentDidMount, reconnects in componentDidUpdate when
roomIdchanges, and disconnects in componentWillUnmount. What replaces all three?Choose an answer to see the explanation.
Question 5 of 8Can you write an error boundary as a function component in React 19?
Choose an answer to see the explanation.
Question 6 of 8What does extending
PureComponentdo?Choose an answer to see the explanation.
Question 7 of 8In React 19, what does
<Badge />show?Read the code, then predictfunction Badge({ label }) { return <span>{label ?? "nothing"}</span>; } Badge.defaultProps = { label: "New" }; export default function App() { return <Badge />; }Choose an answer to see the explanation.
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
Componentand has arendermethod. React makes one object per use, withthis.propsandthis.state. this.setStatemerges an object into the state, one level deep, andthis.statechanges 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.
PureComponentis to classes whatmemois 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
defaultPropson functions andpropTypes, and removed string refs, legacy context, andReactDOM.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.