Every component you've written so far is a function: Welcome, Header, BikeCard, StatusBadge. That's the form you'll use for the rest of the course, and the one the React team officially recommends. But React has more than a decade of history behind it, and for a good chunk of that history the only way to write a component with its own memory was a class. That means the moment you join a project with some mileage on it — or open a 2018 Stack Overflow answer, or read the source of a veteran library — you'll run into class BikeCard extends React.Component, this.props, render(), and bind. This lesson has one very concrete, very practical goal: getting you to read that code comfortably, understand what every piece does, and translate it mentally into modern syntax. You won't be writing new class components; you'll stop being afraid of them.
Contents
- Two syntaxes for the same idea
- Anatomy of a class component:
BikeCardrewritten this.props: the data that arrives from outsideconstructor,this.state, andsetState- The
thisproblem, and whybindshows up everywhere - Full comparison table
- Why React has recommended functions since 2019
- What classes still bring to the table today
- Translating class to function: an equivalence table
- How to approach legacy code in practice
- Two syntaxes for the same idea
Let's start with the essentials: as far as React is concerned, they're the same thing. Both forms produce elements, both get used as <BikeCard />, both take part in reconciliation exactly the same way, and both can live in the same tree. A function can render a class, and a class can render a function, without any trouble.
The difference is in how they're declared and how they access their capabilities.
// Function component: a function that returns JSX
function Welcome() {
return <h1>CicloUrbano</h1>;
}// Class component: a class with a render() method that returns JSX
import { Component } from 'react';
class Welcome extends Component {
render() {
return <h1>CicloUrbano</h1>;
}
}Both are used identically from the outside:
Whoever writes <Welcome /> doesn't know — and doesn't need to know — which of the two forms is behind it. That's exactly why a project can migrate from classes to functions one component at a time, with no big bang required.
A quick note on vocabulary: you'll see functions called function components, or in older documentation, stateless functional components. That last term became obsolete in 2019: since hooks arrived, a function can perfectly well have state.
- Anatomy of a class component:
BikeCard rewritten
BikeCard rewrittenLet's take CicloUrbano's catalogue BikeCard and write it as a class. This code is reading material, not a pattern to imitate.
// src/components/BikeCard.jsx — CLASS VERSION (legacy code)
import { Component } from 'react';
import StatusBadge from './StatusBadge.jsx';
class BikeCard extends Component {
render() {
return (
<article className="bike-card bike-card--urbana">
<h3>
Classic Urban <StatusBadge />
</h3>
<p>Type: urbana</p>
<p>Station: Main Square</p>
<p>
<strong>€2.50 / hour</strong>
</p>
</article>
);
}
}
export default BikeCard;Piece by piece:
| Element | What it means |
|---|---|
import { Component } from 'react' |
Brings in the base class. You'll also see import React from 'react' + extends React.Component |
class BikeCard extends Component |
Declares the class, inheriting from React's base class, which provides this.props, this.state, and setState |
render() |
Mandatory method. React calls it to know what to paint. It's the exact equivalent of a function component's body |
return (...) |
Returns the JSX. Same rules as always: single root, className, self-closing tags |
export default BikeCard |
Identical to the function version |
The key mental equivalence: render() is to a class component what a function's body is to a function component. Everything that lived in your function's return sits inside render() in a class.
Watch this nuance: extends Component is what turns an ordinary class into a React component. Without that inheritance, React doesn't know what to do with it, and it fails to render.
this.props: the data that arrives from outside
this.props: the data that arrives from outsideProps are the data a component receives from its parent. They're the whole subject of the next lesson, Props; here we only care about the access syntax difference, because that's what trips people up the most when reading old code.
In a function, props arrive as a parameter:
In a class, props live in this.props:
Here's the full class version, parameterized with a bike from domain.js:
// CLASS VERSION with props (legacy code)
import { Component } from 'react';
class BikeCard extends Component {
render() {
// Common pattern: pull the props out at the top of render()
// so you don't repeat "this.props." throughout the JSX
const { bike } = this.props;
return (
<article className={`bike-card bike-card--${bike.type}`}>
<h3>{bike.model}</h3>
<p>Type: {bike.type}</p>
<p>Status: {bike.status}</p>
<p>
<strong>€{bike.pricePerHour.toFixed(2)} / hour</strong>
</p>
</article>
);
}
}
export default BikeCard;That const { bike } = this.props; on the first line of render() is a pattern you'll see constantly in legacy code. It's not magic: it's ES6 destructuring applied to this.props.
Two rules apply to both syntaxes, and they're worth stressing right away:
- Props are read-only in both cases. Neither
props.bike = …northis.props.bike = …is valid. React warns you, and the one-way data flow breaks. - The name
propsis a convention, not a reserved keyword. In a function you could name the parameter anything you like; in a class, on the other hand,this.propsis a fixed name imposed by the base class.
constructor, this.state, and setState
constructor, this.state, and setStateState is a component's internal memory, and it's the subject of the Component State lesson. With classes, it's handled with three pieces.
// BookingPanel as a class (legacy code)
import { Component } from 'react';
class BookingPanel extends Component {
// 1. The constructor initializes the state
constructor(props) {
super(props); // MANDATORY before using this
this.state = {
hours: 1
};
}
// 2. A method that updates the state
addHour() {
this.setState({ hours: this.state.hours + 1 });
}
render() {
// 3. Reading the state is this.state
return (
<section className="booking-panel">
<p>Rental hours: {this.state.hours}</p>
</section>
);
}
}
export default BookingPanel;The three pieces, in detail:
constructor(props)+super(props). The constructor is the only place where state gets assigned directly withthis.state = {…}. Thesuper(props)call is mandatory: without it,thisdoesn't exist yet, and JavaScript throws an error. It's one of the classic mistakes when writing classes.this.state. A single object holding all of the component's state. This is an important difference from modern syntax, where you declare an independent piece of state for each value.this.setState({…}). The only valid way to update it. Neverthis.state.hours = 5: that mutates the object without telling React, so no render is triggered and the screen goes stale. On top of that,setStatedoes a shallow merge: the object you pass gets mixed with the existing state, and any keys you don't mention are kept.
// If the state is { hours: 1, type: 'urbana' }
this.setState({ hours: 3 });
// State becomes { hours: 3, type: 'urbana' } -> 'type' is kept on its ownThat automatic merge is a real behavioral difference from modern syntax, where every update replaces the value completely. Keep it in mind when translating code: it's a source of subtle bugs.
- The
this problem, and why bind shows up everywhere
this problem, and why bind shows up everywhereThis is the point where classes earned their bad reputation. In JavaScript, the value of this inside a method depends on how the method is called, not on where it's defined. When you pass a method as an event handler, it gets called "loose" and loses its link to the instance.
// BROKEN: on click, this is undefined and it crashes with
// "Cannot read properties of undefined (reading 'setState')"
class BookingPanel extends Component {
constructor(props) {
super(props);
this.state = { hours: 1 };
}
addHour() {
this.setState({ hours: this.state.hours + 1 }); // <- this isn't the instance
}
render() {
return <button onClick={this.addHour}>Add an hour</button>;
}
}Historically there were three fixes, and you'll run into all three in real code:
// Fix A (the most common in old code): bind in the constructor
constructor(props) {
super(props);
this.state = { hours: 1 };
this.addHour = this.addHour.bind(this); // <- the telltale line
}// Fix C (the most modern within classes): class field with an arrow
addHour = () => {
this.setState({ hours: this.state.hours + 1 });
};Arrow functions don't have their own this: they take it from the scope where they were defined, which here is the instance. That's why B and C work.
When you open a file and see a constructor with four lines of this.something = this.something.bind(this), you know exactly what you're looking at: class code solving the this problem. In function components this problem simply doesn't exist, because there's no this anywhere. It's one of the biggest simplifications modern syntax brought.
- Full comparison table
| Aspect | Function component | Class component |
|---|---|---|
| Declaration | function X() { … } |
class X extends Component { render() { … } } |
| Verbosity | Minimal: 3 lines for a simple component | High: class + render() + often a constructor |
this |
Doesn't exist. Nothing to bind | Central and problematic: requires bind or arrows |
| Accessing props | Parameter: props.bike or destructuring in the signature |
this.props.bike |
| Accessing state | One independent value per piece of data | A single this.state object |
| Updating state | An updater function per piece of data; replaces the value | this.setState; shallow-merges |
| Side effects and lifecycle | Hooks (Module 5) | Lifecycle methods (04-03) |
| Reusing logic | Custom hooks: simple composition | HOCs and render props: much more ceremony and nesting |
| Performance | Equivalent. The practical difference is negligible; functions are somewhat lighter to transpile and allow future compiler optimizations | Equivalent |
| Code size | Smaller: less text for the same functionality | Larger |
| Official docs | The only form taught since 2023 | Appears in a legacy API section |
| Community support | Every modern library assumes functions | Compatible, but fewer new examples every year |
| Still supported in React 19? | Yes, it's the recommended form | Yes, there's no plan to remove it |
| When to use it | Always, in new code | Only when maintaining existing code and for error boundaries |
- Why React has recommended functions since 2019
In February 2019, React 16.8 introduced hooks, and with them functions stopped being limited: they could hold state, run effects, and access context. From that point on, the React team discouraged classes for new code. The reasons, in order of importance:
-
Reusing stateful logic was very hard. With classes, sharing behavior between components forced you into patterns like higher-order components or render props, which produced trees with five or six layers of meaningless wrapping (what came to be known as wrapper hell). Custom hooks solve this with a simple function call.
-
Related logic ended up scattered. In a class, subscribing to something and cleaning it up lived in separate lifecycle methods, far apart from each other, while a single method mixed together concerns that had nothing to do with one another. With hooks, each concern gets grouped in its own block.
-
thisconfused both people and machines. Beyond the binding problem you just saw,thisgets in the way of automatic optimizations: a class is harder to statically analyze than a function. -
Less noise, less surface for error. Comparing the two versions of
BikeCardmakes the point: the class version needs an extraimport, a class declaration, arendermethod, and an extra level of indentation to say exactly the same thing.
Very important: classes aren't deprecated, and they aren't going away. The React team has been explicit that there are no plans to remove them, and that existing code will keep working. There's no urgency to migrate a project that already works. What there is, is a clear recommendation: everything new, with functions.
- What classes still bring to the table today
A single thing, and it's worth pinning down precisely:
Error boundaries can only be written with class components.
An error boundary is a component that catches errors thrown by its children during render and shows a fallback interface instead of letting the whole application go blank. It needs the static getDerivedStateFromError and componentDidCatch methods, which have no hook equivalent. It's the subject of the Error Boundaries lesson, where you'll write the only "real" class component of the course.
In practice this changes nothing about how you'll work, for two reasons: error boundaries are one or two components in an entire application, and libraries like react-error-boundary already wrap them so you don't even have to write them yourself.
Outside that one case, there's no capability left that's exclusive to classes. Everything else — state, effects, context, DOM references, memoization — has its hook equivalent.
- Translating class to function: an equivalence table
This table is your dictionary for reading legacy code. You don't need to have mastered the right-hand column yet — each hook is covered in depth in Module 5. Use it as a translation reference.
| In a class | In a function |
|---|---|
class X extends Component |
function X() |
The body of render() |
The body of the function |
this.props.bike |
The parameter props.bike, or { bike } in the signature |
this.state = { hours: 1 } in the constructor |
useState(1) for the hours value |
this.state.hours |
The hours variable |
this.setState({ hours: 3 }) |
The updater function for hours |
| Automatic state merging | None: each value is independent |
constructor for initialization |
Initialization happens right in the useState call |
this.addHour.bind(this) |
Nothing. The problem disappears |
componentDidMount |
useEffect with an empty dependency list |
componentDidUpdate |
useEffect with dependencies |
componentWillUnmount |
The cleanup function returned by useEffect |
shouldComponentUpdate |
React.memo (08-02) |
this.myNode with createRef |
useRef (05-03) |
static contextType |
useContext (05-04) |
| HOC or render props to share logic | A custom hook (05-06) |
static getDerivedStateFromError |
No equivalent: requires a class |
Here's the full translation of BookingPanel, so you can see the reduction all at once:
// BEFORE: class (legacy code)
import { Component } from 'react';
class BookingPanel extends Component {
constructor(props) {
super(props);
this.state = { hours: 1 };
this.addHour = this.addHour.bind(this);
}
addHour() {
this.setState({ hours: this.state.hours + 1 });
}
render() {
return (
<section className="booking-panel">
<p>Rental hours: {this.state.hours}</p>
<button onClick={this.addHour}>Add an hour</button>
</section>
);
}
}
export default BookingPanel;// AFTER: function with hooks (the form the rest of the course uses)
import { useState } from 'react';
function BookingPanel() {
const [hours, setHours] = useState(1);
return (
<section className="booking-panel">
<p>Rental hours: {hours}</p>
<button onClick={() => setHours(hours + 1)}>Add an hour</button>
</section>
);
}
export default BookingPanel;Twenty-five lines versus twelve, with no this, no constructor, and no bind. And you don't need to understand yet how useState works under the hood to appreciate the difference: you'll see that in the next lesson on state, and in depth in Module 5.
- How to approach legacy code in practice
A sensible playbook for when you land in a project full of classes:
flowchart TD
A[I find a class component] --> B{Do I need to change it?}
B -- No --> C[Leave it. It works just as well]
B -- Yes --> D{Is the change small?}
D -- Yes --> E[Make it in the class itself.<br/>Don't mix a fix with a migration]
D -- No, it's a rewrite --> F{Is there test coverage?}
F -- Yes --> G[Migrate to a function, leaning on the tests]
F -- No --> H[Write the tests first.<br/>Then migrate]
Three field tips:
- Migrating for migrating's sake adds no value. A class component that's stable, tested, and nobody touches isn't technical debt: it's code that works. Prioritize migrating the ones you're going to be working in anyway.
- Don't mix migration and functional changes in the same commit. If something breaks, you won't know whether it was the translation or the new functionality.
- Watch out for
setStatemerging. It's the number one trap when translating: athis.setState({ a: 1 })keepsbwithout saying so, and the version with separate hooks has to be written with that in mind.
And as for CicloUrbano: the rest of the course is written entirely with function components. The one exception is the error boundary in lesson 04-05, where a class is mandatory by React's design. The class version of BikeCard you've read here stays as a reading exercise; your project keeps the function version.
Common Mistakes and Tips
- Forgetting
super(props)in the constructor. The error isMust call a super constructor in derived class before accessing 'this'. If you write a class and don't define your own constructor, there's no problem; if you do define one,super(props)always goes on the first line. - Mutating
this.statedirectly.this.state.hours = 5doesn't throw a visible error, but React never finds out, and the screen doesn't change. Alwaysthis.setState. - Reading
this.stateright aftersetStateand expecting the new value. Updates are batched and applied afterward; on the next line you'll still see the old value. It's the same behavior you'll study with modern syntax in Component State. - Writing
render()under a different name. The method must be called exactlyrender. ARender()or arenderComponent()produces the errorObjects are not valid as a React child, or just an empty screen. - Turning a class into a function by copying only the body of
render(). You also need to review the constructor, helper methods,bindcalls, and lifecycle methods.render()is only one part of it. - Tip: when you see
this.in a React file, you already know what you're looking at. It's the infallible marker of a class component. - Tip: don't memorize class syntax — memorize the translation table. The goal is to read, not to produce.
Exercises
Exercise 1
Translate this CicloUrbano class component into a function component. You won't need hooks: it has no state.
import { Component } from 'react';
class StationCard extends Component {
render() {
const { station } = this.props;
return (
<article className="station-card">
<h3>{station.name}</h3>
<p>District: {station.district}</p>
<p>Docks: {station.docks}</p>
</article>
);
}
}
export default StationCard;Exercise 2
The following class component fails on button click with the message Cannot read properties of undefined (reading 'setState'). Explain precisely why that happens, and give two different fixes, both within class syntax itself.
import { Component } from 'react';
class StationAvailability extends Component {
constructor(props) {
super(props);
this.state = { free: 12 };
}
takeDock() {
this.setState({ free: this.state.free - 1 });
}
render() {
return (
<section>
<p>Free docks at Main Square: {this.state.free}</p>
<button onClick={this.takeDock}>Take a dock</button>
</section>
);
}
}Exercise 3
A colleague claims: "Let's rewrite the thirty class components in the project as functions, because classes are deprecated and they also perform worse." Evaluate both technical claims and propose a reasonable strategy. Also state whether there's any component that can't be migrated.
Solutions
Solution 1.
// src/components/StationCard.jsx
function StationCard({ station }) {
return (
<article className="station-card">
<h3>{station.name}</h3>
<p>District: {station.district}</p>
<p>Docks: {station.docks}</p>
</article>
);
}
export default StationCard;The changes, one by one:
| Removed | Replaced with |
|---|---|
import { Component } from 'react' |
Nothing: a function doesn't need anything from React to exist |
class … extends Component |
function StationCard(…) |
render() { … } |
The body of the function |
const { station } = this.props; |
Destructuring right in the signature: ({ station }) |
Eleven lines of ceremony become zero. The JSX is identical, because the JSX never depended on the component's syntax.
Solution 2.
Why it fails. In render(), this.takeDock isn't called: it's passed by reference to onClick. When React later invokes that handler, it does so as a loose function, with no object in front of the dot. In a JavaScript class (which runs in strict mode), the this of a function invoked that way is undefined, so this.setState tries to read a property off undefined and throws.
Fix A: bind in the constructor.
constructor(props) {
super(props);
this.state = { free: 12 };
this.takeDock = this.takeDock.bind(this);
}Fix B: declare the method as a class field with an arrow function.
A third option would be wrapping the call in the JSX: onClick={() => this.takeDock()}. It works, but it creates a new function on every render, which has implications you'll see in Module 8.
In a function component this problem can't happen, because there's no this to bind anything to.
Solution 3.
"Classes are deprecated": false. They aren't marked as deprecated, and there are no plans to remove them. They're discouraged for new code, which is a different thing: existing code will keep working in React 19 and in the versions after it.
"They perform worse": false in practice. The performance difference between the two syntaxes is negligible and will never be the cause of a real-world problem. Bottlenecks come from unnecessary renders, huge lists, or heavy work inside render, not from how a component is declared. And those causes get tackled the same way in either syntax.
A reasonable strategy:
- Freeze the growth: every new component gets written as a function. That alone means the share of legacy code can only go down.
- Opportunistic migration: when a class component needs to be touched for a functional reason, migrate it then, in a separate commit from the functional change.
- Prioritize by real pain: start with components riddled with
bindcalls, with duplicated logic betweencomponentDidMountandcomponentDidUpdate, or wrapped in several HOCs. That's where migration pays for itself. - Cover critical components with tests before migrating them (Module 9).
- Never migrate "in bulk" without a reason. Thirty simultaneous rewrites are thirty chances to introduce a regression in exchange for zero value to the user.
Components that can't be migrated: error boundaries. static getDerivedStateFromError and componentDidCatch have no hook equivalent, so those components stay as classes (or you delegate to a library that wraps them). They're covered in Error Boundaries.
Conclusion
You now know how to read both ways of writing a React component. A class component extends Component, must implement render(), accesses data through this.props, keeps its memory in a single this.state object that's updated with this.setState — with a shallow merge — and carries the this problem, which explains the bind calls scattered through its constructors. A function component does the same thing without a class, without render(), without a constructor, and without this, with less code and with hooks for everything classes used to solve with lifecycle methods.
You also have the why behind the change: hooks, in 2019, removed the one advantage classes still had — state — and solved the problem of reusing logic far better. And you have the exception clearly bounded: error boundaries still require a class, and you'll see them in 04-05. Outside that, everything else in the course — and everything you write in a modern project — will be a function component.
With both syntaxes clear, let's get back to the ceiling we left hanging in the previous lesson: our three BikeCards still show the same bike because the data is written inside the component. In the next lesson, Props: Passing Data to Components, we'll open that component up to the outside world: you'll learn to parameterize it, destructure its props in the signature, give them default values, wrap content with children, and understand why props are read-only. From there, CicloUrbano's catalogue will stop being a mockup and start showing real data.
React Course
Module 1: Getting Started with React
- What Is React?
- Setting Up the Development Environment
- Hello World in React
- JSX: A JavaScript Syntax Extension
- How React Renders: Virtual DOM and Reconciliation
Module 2: React Components
- Understanding Components
- Function vs Class Components
- Props: Passing Data to Components
- State: Managing Component State
- Styling Components: CSS, Modules and Utilities
Module 3: Working with Events
- Handling Events in React
- Conditional Rendering
- Lists and Keys
- Forms and Controlled Components
- Form Validation and Uncontrolled Components
- Accessibility in Interactive Components
Module 4: Advanced Component Concepts
- Lifting State Up
- Composition vs Inheritance
- React Lifecycle Methods
- Hooks: Introduction and Basic Use
- Error Boundaries: Catching Failures in the UI
Module 5: React Hooks
- The useState Hook
- The useEffect Hook
- The useRef Hook and DOM Access
- The useContext Hook
- The useReducer Hook
- Custom Hooks
Module 6: Routing in React
- Introducing React Router
- Setting Up React Router
- Nested Routes
- Programmatic Navigation
- Protected Routes and Access Control
Module 7: State Management
- Introduction to State Management
- The Context API
- Redux: Introduction and Setup
- Redux: Actions and Reducers
- Redux: Connecting to React
- Server State: Fetching, Caching and Syncing
Module 8: Performance Optimization
- Performance Optimization Techniques in React
- Memoization with React.memo
- The useMemo and useCallback Hooks
- Code Splitting and Lazy Loading
- Measuring Performance with React DevTools Profiler
Module 9: Testing React Applications
- Introduction to Testing
- Unit Testing with Jest
- Component Testing with React Testing Library
- Testing Asynchronous Code and Mocking APIs
- End-to-End Testing with Cypress
Module 10: Advanced Topics
- Server-Side Rendering (SSR) with Next.js
- Static Site Generation (SSG) with Next.js
- Suspense and React Server Components
- TypeScript with React
- React Native: Building Mobile Apps
