Class Components (Legacy)
Before hooks arrived in React 16.8 (February 2019), components that needed state or lifecycle logic had to be written as JavaScript classes. Many large apps, libraries, Stack Overflow answers and exam questions still use them. React 19 still supports class components, but new code should use function components and hooks.
In this lesson you will learn to read class components: render, props, this.state and setState, the main lifecycle methods and how they map to useEffect, error boundaries (the one feature that still needs a class), and how to convert a class to a function.
A class component
A class component extends Component from react and must have a render() method that returns JSX. Props are read from this.props.
import { Component } from "react";
class Welcome extends Component {
render() {
return <h2>Welcome to the Pune branch, {this.props.name}</h2>;
}
}
// The same component as a function:
function WelcomeFn({ name }) {
return <h2>Welcome to the Pune branch, {name}</h2>;
}Both are used the same way: <Welcome name="Rohan" />. The parent cannot tell the difference.
State and setState
A class keeps all its state in one object, this.state, and changes it with this.setState(). Unlike the useState setter, setState merges the object you pass into the existing state, so you only list the fields that change.
import { Component } from "react";
import { createRoot } from "react-dom/client";
class AttendanceCounter extends Component {
state = { present: 0, absent: 0 }; // class field: the initial state
markPresent = () => {
// arrow function field: "this" always points to the component
this.setState((prev) => ({ present: prev.present + 1 }));
};
markAbsent = () => {
this.setState((prev) => ({ absent: prev.absent + 1 }));
};
componentDidMount() {
console.log(`Register opened for ${this.props.className}`);
}
componentDidUpdate(prevProps, prevState) {
if (prevState.present !== this.state.present) {
console.log(`Present count changed to ${this.state.present}`);
}
}
componentWillUnmount() {
console.log("Register closed");
}
render() {
const { present, absent } = this.state;
return (
<div>
<h3>Class {this.props.className}: {present} present, {absent} absent</h3>
<button onClick={this.markPresent}>Present</button>{" "}
<button onClick={this.markAbsent}>Absent</button>
</div>
);
}
}
class App extends Component {
state = { open: true };
render() {
return (
<>
<button onClick={() => this.setState({ open: !this.state.open })}>Toggle register</button>
{this.state.open && <AttendanceCounter className="10-B" />}
</>
);
}
}
createRoot(document.getElementById("root")).render(<App />);Loading the example, clicking "Present" once, "Absent" once and then "Toggle register" prints:
Register opened for 10-B Present count changed to 1 Register closed
Points to notice:
state = {...}as a class field sets the initial state. Older code does this in a constructor:constructor(props) { super(props); this.state = {...}; }.setStateaccepts an object or an updater function(prev) => changes, just like the hook setter. The same "snapshot" rules apply:this.statedoes not change immediately after calling it.- Handlers are written as arrow function fields (
markPresent = () => {...}) so thatthisstill refers to the component when React calls them. With normal methods, older code had to writethis.markPresent = this.markPresent.bind(this)in the constructor. Forgetting it causes the classic error "Cannot read properties of undefined (reading 'setState')".
Lifecycle methods and their hook equivalents
Class components react to moments in a component's life with special methods. The three you will meet most:
| Class method | When it runs | Function component equivalent |
|---|---|---|
componentDidMount() | After the first render is on screen | useEffect(() => {...}, []) |
componentDidUpdate(prevProps, prevState) | After every later update | useEffect(() => {...}, [deps]) |
componentWillUnmount() | Just before removal | The cleanup function returned from an effect |
shouldComponentUpdate() / PureComponent | Before re-rendering, to skip it | memo |
Class code often has to repeat logic across methods: subscribe in componentDidMount, re-subscribe in componentDidUpdate when an id changes, unsubscribe in componentWillUnmount. A single useEffect with a dependency array and a cleanup expresses all three, which is one reason hooks replaced classes.
You may also see methods prefixed with UNSAFE_, such as UNSAFE_componentWillMount or UNSAFE_componentWillReceiveProps. They are deprecated, cause bugs with modern React features, and should be removed when you touch that code.
Error boundaries: still a class
An error boundary catches errors thrown while rendering its children and shows a fallback instead of a blank screen. React only supports error boundaries as class components with static getDerivedStateFromError (to switch to the fallback) and optionally componentDidCatch (to log the error):
import { Component, useState } from "react";
import { createRoot } from "react-dom/client";
class ErrorBoundary extends Component {
state = { hasError: false };
static getDerivedStateFromError() {
return { hasError: true }; // render the fallback next time
}
componentDidCatch(error) {
console.log(`Logged for the support team: ${error.message}`);
}
render() {
if (this.state.hasError) {
return <p style={{ color: "crimson" }}>{this.props.fallback}</p>;
}
return this.props.children;
}
}
function FareWidget() {
const [route, setRoute] = useState({ from: "Jaipur", to: "Delhi", fare: 655 });
return (
<div>
<p>{route.from} to {route.to}: ₹{route.fare.toFixed(2)}</p>
<button onClick={() => setRoute({ from: "Jaipur", to: "Agra" })}>Load Agra fare (buggy)</button>
</div>
);
}
createRoot(document.getElementById("root")).render(
<>
<h3>Bus booking</h3>
<ErrorBoundary fallback="Fares are unavailable right now. Please try again later.">
<FareWidget />
</ErrorBoundary>
<p>The rest of the page keeps working.</p>
</>
);The buggy button sets a route without a fare, so route.fare.toFixed throws during render. Clicking it replaces only the widget with the fallback message, while the heading and the last paragraph stay. In the console, React first reports the caught error with its stack trace, and then componentDidCatch adds the line "Logged for the support team: Cannot read properties of undefined (reading 'toFixed')".
Error boundaries catch errors during rendering and in lifecycle methods, not in event handlers or async code (use try...catch there). Most projects use the react-error-boundary package, which wraps this class for you, as shown in Suspense, lazy & use().
Converting a class to a function
When you maintain old code, converting a class to a function usually makes it shorter and clearer:
// Before: class
class Clock extends Component {
state = { now: new Date() };
componentDidMount() {
this.timer = setInterval(() => this.setState({ now: new Date() }), 1000);
}
componentWillUnmount() {
clearInterval(this.timer);
}
render() {
return <p>{this.props.city}: {this.state.now.toLocaleTimeString("en-IN")}</p>;
}
}
// After: function with hooks
function ClockFn({ city }) {
const [now, setNow] = useState(() => new Date());
useEffect(() => {
const timer = setInterval(() => setNow(new Date()), 1000);
return () => clearInterval(timer);
}, []);
return <p>{city}: {now.toLocaleTimeString("en-IN")}</p>;
}Map this.props.x to props, each field of this.state to a useState, instance variables such as this.timer to a local variable or useRef, and lifecycle methods to effects.
Common mistakes
- Mutating
this.state.this.state.present++does not re-render. Always callsetState. - Losing
this. Passing a normal method asonClick={this.handleClick}without binding losesthis. Use arrow function fields. - Expecting
setStateto replace state. It merges at the top level; nested objects still need spreading. - Writing new class components. Use function components for new code; keep classes only for error boundaries or legacy maintenance.
Key points
- A class component extends
Component, readsthis.propsand returns JSX fromrender(). - State is one object,
this.state, updated withthis.setState, which merges changes. - Use arrow function fields for handlers so
thisstays correct. componentDidMount,componentDidUpdateandcomponentWillUnmountmap touseEffectand its cleanup.- Error boundaries still require a class with
getDerivedStateFromError. - Prefer function components and hooks for all new code.
Exercise
Convert this class component to a function component with hooks. It shows a greeting and a count of how many times the "Wave" button was clicked, and logs "Waved at NAME" each time the count changes after the first render. class Greeter extends Component { state = { waves: 0 }; componentDidUpdate(prevProps, prevState) { if (prevState.waves !== this.state.waves) console.log("Waved at " + this.props.name); } render() { return <button onClick={() => this.setState({ waves: this.state.waves + 1 })}>Wave at {this.props.name} ({this.state.waves})</button>; } }
Show solution
import { useState, useEffect, useRef } from "react";
import { createRoot } from "react-dom/client";
function Greeter({ name }) {
const [waves, setWaves] = useState(0);
const firstRender = useRef(true);
useEffect(() => {
if (firstRender.current) {
firstRender.current = false; // componentDidUpdate does not run on mount
return;
}
console.log(`Waved at ${name}`);
}, [waves, name]);
return <button onClick={() => setWaves(waves + 1)}>Wave at {name} ({waves})</button>;
}
createRoot(document.getElementById("root")).render(<Greeter name="Isha" />);this.state.waves becomes a useState variable. componentDidUpdate becomes an effect that skips its first run with a ref. A simpler modern design would log directly in the click handler, since logging is a reaction to the click.
Quick Quiz
1. How does a class component read its props?
Class components receive props on the instance as this.props.
2. What does this.setState({ present: 5 }) do to other fields in this.state?
Class setState shallowly merges the object you pass with the current state.
3. Which hook pattern replaces componentDidMount plus componentWillUnmount?
The effect runs after the first render and its cleanup runs before removal.
4. Which feature still requires a class component in React 19?
React offers error boundaries only through getDerivedStateFromError and componentDidCatch on classes.
Learn React with a mentor
Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
