Functional components with Hooks are the default in new React code, but Class Components have not vanished. They remain the load-bearing structure of an enormous volume of production software: banking dashboards written between 2016 and 2020, internal tooling at companies that migrate slowly, and any codebase old enough to predate Hooks (introduced in React 16.8, February 2019). If you inherit one of these systems, you either understand class instances, this binding, and lifecycle methods, or you are blocked.

Declarative Functions vs. Imperative Instances

A functional component is a plain function: props in, JSX out. Hooks like useState and useEffect attach behavior to that function via a hidden linked list managed by React's fiber architecture, but the function itself has no persistent identity between renders — React just calls it again.

A Class Component is different in kind, not just in syntax. React instantiates the class once with new and keeps that instance alive for the entire lifetime of the component in the tree. this.state, this.props, and any instance properties you define (timers, subscriptions, DOM refs) persist on that object across every re-render. Every lifecycle method is a distinct, explicitly named entry point React calls at an exact, documented moment — there is no ambiguity about when componentDidMount runs versus when useEffect with an empty dependency array runs relative to paint. This explicitness is exactly why understanding classes builds a correct mental model of React's internals, even if you write Hooks day to day.


We will use Vite, not Create React App (CRA was officially deprecated by the React team in February 2025). Vite gives native ESM dev serving, near-instant HMR, and a Rollup production build with no custom webpack configuration required.

terminalbash
npm create vite@latest react-class-masterclass -- --template react
cd react-class-masterclass
npm install
npm run dev

Anatomy of vite.config.js and package.json

vite.config.jsjavascript
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

// https://vite.dev/config/
export default defineConfig({
  // @vitejs/plugin-react enables Fast Refresh: component state (including
  // class instance state during dev) survives most edits without a full reload.
  plugins: [react()],
  server: {
    port: 5173,
    open: true
  },
  build: {
    target: 'es2020',
    sourcemap: true
  }
})
package.jsonjson
{
  "name": "react-class-masterclass",
  "private": true,
  "version": "0.0.1",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "react": "^19.1.0",
    "react-dom": "^19.1.0"
  },
  "devDependencies": {
    "@vitejs/plugin-react": "^4.3.4",
    "vite": "^6.0.0"
  }
}

@vitejs/plugin-react uses Babel to apply the automatic JSX runtime, so you do not need import React from 'react' at the top of every file just to use JSX. You still need to import React explicitly if you reference React.Component, React.Fragment, or other named exports directly.

Entry Points: main.jsx and App.jsx

src/main.jsxjavascript
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import App from './App.jsx'
import './index.css'

// createRoot enables React 18+ concurrent features. StrictMode intentionally
// double-invokes render-phase lifecycle methods (constructor, render,
// getDerivedStateFromProps, shouldComponentUpdate) in development only, to
// surface impure side effects hidden in what should be pure functions.
createRoot(document.getElementById('root')).render(
  <StrictMode>
    <App />
  </StrictMode>
)
src/App.jsxjavascript
import { Component } from 'react'
import Greeter from './components/Greeter.jsx'

class App extends Component {
  render() {
    return (
      <div className="app-shell">
        <Greeter name="Enterprise Team" />
      </div>
    )
  }
}

export default App

Every Class Component must extend React.Component (or Component, imported by name) and implement exactly one required method: render(). Everything else — state, handlers, other lifecycle methods — is optional.

src/components/Greeter.jsxjavascript
import { Component } from 'react'

class Greeter extends Component {
  constructor(props) {
    // super(props) is mandatory before any other statement in the constructor.
    // It runs React.Component's own constructor, which wires up `this.props`.
    // Skip it, or call it without `props`, and `this.props` is `undefined`
    // inside the constructor body (though React still assigns it correctly
    // afterward for render() and every other method).
    super(props)

    this.state = {
      clickCount: 0
    }

    // Class methods are NOT auto-bound to `this`. When React invokes
    // this.handleClick as a bare callback (e.g. onClick={this.handleClick}),
    // it loses its receiver — `this` inside the method becomes `undefined`
    // in strict mode (which ES modules always are). Binding in the
    // constructor fixes `this` once, permanently, for the life of the instance.
    this.handleClick = this.handleClick.bind(this)
  }

  handleClick() {
    // Functional updater form: React batches setState calls and guarantees
    // `prevState` reflects the most recent state, even inside rapid
    // successive calls within the same event handler.
    this.setState((prevState) => ({
      clickCount: prevState.clickCount + 1
    }))
  }

  render() {
    // render() must be a pure function of this.props and this.state.
    // No side effects, no direct DOM mutation, no calling setState here —
    // doing so causes React to throw or infinitely re-render.
    const { name } = this.props
    const { clickCount } = this.state

    return (
      <div className="greeter">
        <p>Hello, {name}. You clicked {clickCount} time{clickCount === 1 ? '' : 's'}.</p>
        <button type="button" onClick={this.handleClick}>
          Click me
        </button>
      </div>
    )
  }
}

export default Greeter
src/components/GreeterClassField.jsx (equivalent, alternate style)javascript
import { Component } from 'react'

class GreeterClassField extends Component {
  state = {
    clickCount: 0
  }

  // Class field arrow function: bound once per instance, no constructor needed.
  handleClick = () => {
    this.setState((prevState) => ({
      clickCount: prevState.clickCount + 1
    }))
  }

  render() {
    const { name } = this.props
    const { clickCount } = this.state

    return (
      <div className="greeter">
        <p>Hello, {name}. You clicked {clickCount} time{clickCount === 1 ? '' : 's'}.</p>
        <button type="button" onClick={this.handleClick}>
          Click me
        </button>
      </div>
    )
  }
}

export default GreeterClassField

Module 1 Takeaways

  • Class Components are not legacy cruft — Error Boundaries have no Hook equivalent and require a class.
  • React instantiates a class once with new and keeps this alive across every re-render; functional components have no such persistent identity.
  • Vite + @vitejs/plugin-react replaces Create React App; the automatic JSX runtime means you don't need import React just for JSX.
  • StrictMode double-invokes render-phase methods in development to catch impure side effects — this is intentional, not a bug.
  • super(props) must be the first statement in any constructor that defines one.
  • Class methods need explicit .bind(this) in the constructor, or must be written as arrow-function class fields, to preserve this when passed as callbacks.