#Class components
AddedBefore hooks arrived in React 16.8 (February 2019), a component that needed state or needed to do something after it appeared on screen had to be a class. A class component extends React.Component, receives its props on this.props, and has one required method, render(), that returns JSX. React creates one instance of the class per place it appears in the tree and keeps that instance alive for as long as the component is mounted — the instance is where state, timers and subscriptions live.
The mental model: a function component is a function React calls on every render; a class component is an object React creates once and then calls render() on each time it needs fresh JSX. Everything that has to survive between renders hangs off this. That one difference explains most of what follows — including the most common class-component bug, losing this.
Class components are not deprecated. React 19.3 still fully supports them; the React team just recommends function components for new code. You'll meet them in older codebases, in interview questions ("explain the lifecycle"), and in every error boundary, because there is still no hook for catching render errors.
import { Component } from "react";
// Class component (the pre-2019 way)
class Greeting extends Component {
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}
// Function component (the modern way) — same output
function GreetingFn({ name }) {
return <h1>Hello, {name}</h1>;
}
<Greeting name="Rohit" />; // <h1>Hello, Rohit</h1>
<GreetingFn name="Rohit" />; // <h1>Hello, Rohit</h1>class Greeting extends ComponentmakesGreetinga subclass of React's base class. The base class is what gives the instancethis.props,this.setStateandthis.forceUpdate, and it's how React recognises a class component (it checksComponent.prototype.isReactComponent).- When
<Greeting name="Rohit" />mounts, React does roughlynew Greeting(props), stores the instance, setsinstance.props = { name: "Rohit" }, and callsinstance.render(). render()readsthis.props.nameand returns<h1>Hello, Rohit</h1>. It must be pure — same props and state, same JSX, no side effects — exactly like a function component's body.- On a later re-render React doesn't create a new instance: it updates
this.propson the same object and callsrender()again. A function component, by contrast, is simply called again with new arguments. - Both versions produce identical DOM (the test renders both and finds two
<h1>Hello, Rohit</h1>). The difference is where the state and side effects would live: onthisfor the class, in hooks for the function.
Handlers and this
The classic class bug: you pass a method as an event handler, React later calls it as a plain function, and this is undefined.
class Broken extends Component {
state = { count: 0 };
handleClick() {
this.setState({ count: this.state.count + 1 });
}
render() {
// ❌ passes the bare function; React calls it without a receiver
return <button onClick={this.handleClick}>Clicked {this.state.count}</button>;
}
}
// Click → TypeError: Cannot read properties of undefined (reading 'setState')
// Fix 1 (the old idiom): bind once in the constructor
class BoundInConstructor extends Component {
constructor(props) {
super(props);
this.state = { count: 0 };
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
this.setState({ count: this.state.count + 1 });
}
render() {
return <button onClick={this.handleClick}>Clicked {this.state.count}</button>;
}
}
// Fix 2 (what most class code used later): an arrow function in a class field
class ClassField extends Component {
state = { count: 0 };
handleClick = () => {
this.setState({ count: this.state.count + 1 });
};
render() {
return <button onClick={this.handleClick}>Clicked {this.state.count}</button>;
}
}onClick={this.handleClick}reads the method off the instance and hands React the function alone. The link to the instance is lost at that moment — this is plain JavaScript method extraction, nothing React-specific.- When you click, React calls the handler like
handler(event). Class bodies are always in strict mode, so a function called without a receiver getsthis === undefined. this.setStatethen throws. The test caught the real message:TypeError: Cannot read properties of undefined (reading 'setState'), and the button stayed atClicked 0.- Fix 1:
this.handleClick.bind(this)in the constructor creates a new function withthispermanently set to the instance, and stores it as an own property that shadows the prototype method.super(props)must come first — you can't touchthisin a subclass constructor before callingsuper. - Fix 2:
handleClick = () => { … }is a class field, so it's created per instance in the constructor, and an arrow function takesthisfrom where it was defined — the instance. Both fixed buttons went toClicked 1after one click. - Function components never have this problem because there is no
this: a handler is a closure over the render's variables.
A quick check of what actually happens in 19.3 (tested in the sandbox):
class StringRef extends Component {
render() {
return <input ref="name" />; // string ref — removed in React 19
}
}
// render(<StringRef />) throws:
// "Expected ref to be a function, an object returned by React.createRef(), or undefined/null."
function FnDefaults({ size }) {
return <p>size={String(size)}</p>;
}
FnDefaults.defaultProps = { size: "md" }; // ignored for function components in React 19
class ClassDefaults extends Component {
static defaultProps = { size: "md" }; // still works for classes
render() {
return <p>size={this.props.size}</p>;
}
}
// <FnDefaults /> → size=undefined
// <ClassDefaults /> → size=md- String refs were a React 16-and-earlier way to name a DOM node (
this.refs.name). React 19 doesn't recognise a string as a ref any more, so rendering throws immediately with the message quoted above. The fix iscreateRef()in a class oruseRef()in a function (Chapter 6). defaultPropson a function component is silently ignored in React 19 — no warning, the prop is justundefined. Use a default parameter ({ size = "md" }) instead.static defaultPropson a class still works, because classes have no destructuring-default equivalent. That's why you'll still see it in class code.