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

  1. Two syntaxes for the same idea
  2. Anatomy of a class component: BikeCard rewritten
  3. this.props: the data that arrives from outside
  4. constructor, this.state, and setState
  5. The this problem, and why bind shows up everywhere
  6. Full comparison table
  7. Why React has recommended functions since 2019
  8. What classes still bring to the table today
  9. Translating class to function: an equivalence table
  10. How to approach legacy code in practice

  1. 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:

<Welcome />

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.

  1. Anatomy of a class component: BikeCard rewritten

Let'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.

  1. this.props: the data that arrives from outside

Props 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:

function BikeCard(props) {
  return <h3>{props.bike.model}</h3>;
}

In a class, props live in this.props:

class BikeCard extends Component {
  render() {
    return <h3>{this.props.bike.model}</h3>;
  }
}

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 = … nor this.props.bike = … is valid. React warns you, and the one-way data flow breaks.
  • The name props is 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.props is a fixed name imposed by the base class.

  1. constructor, this.state, and setState

State 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 with this.state = {…}. The super(props) call is mandatory: without it, this doesn'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. Never this.state.hours = 5: that mutates the object without telling React, so no render is triggered and the screen goes stale. On top of that, setState does 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 own

That 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.

  1. The this problem, and why bind shows up everywhere

This 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 B: arrow function in the JSX
<button onClick={() => this.addHour()}>Add an hour</button>
// 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.

  1. 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

  1. 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:

  1. 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.

  2. 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.

  3. this confused both people and machines. Beyond the binding problem you just saw, this gets in the way of automatic optimizations: a class is harder to statically analyze than a function.

  4. Less noise, less surface for error. Comparing the two versions of BikeCard makes the point: the class version needs an extra import, a class declaration, a render method, 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.

  1. 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.

  1. 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.

  1. 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 setState merging. It's the number one trap when translating: a this.setState({ a: 1 }) keeps b without 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 is Must 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.state directly. this.state.hours = 5 doesn't throw a visible error, but React never finds out, and the screen doesn't change. Always this.setState.
  • Reading this.state right after setState and 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 exactly render. A Render() or a renderComponent() produces the error Objects 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, bind calls, 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.

takeDock = () => {
  this.setState({ free: this.state.free - 1 });
};

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:

  1. Freeze the growth: every new component gets written as a function. That alone means the share of legacy code can only go down.
  2. 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.
  3. Prioritize by real pain: start with components riddled with bind calls, with duplicated logic between componentDidMount and componentDidUpdate, or wrapped in several HOCs. That's where migration pays for itself.
  4. Cover critical components with tests before migrating them (Module 9).
  5. 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

Module 2: React Components

Module 3: Working with Events

Module 4: Advanced Component Concepts

Module 5: React Hooks

Module 6: Routing in React

Module 7: State Management

Module 8: Performance Optimization

Module 9: Testing React Applications

Module 10: Advanced Topics

Module 11: Project: Building a Complete Application

© Copyright 2026. All rights reserved