Class Components (Legacy)
Before React 16.8, class components were the only way to use state and lifecycle methods. They extend React.Component, define a render() method, and access props and state through this. Class components still work in React 18 — React has not deprecated them — but the React team recommends function components with hooks for all new code.
Understanding class components is useful for reading older codebases, working in large legacy applications, and understanding why hooks were designed the way they were.
Basic Syntax
import { Component } from 'react'
class Welcome extends Component {
render() {
return <h1>Hello, class component!</h1>
}
}Every class component must implement the render() method. React calls it to get the JSX description of the UI. The return value follows the same rules as a function component's return value.
Props in Class Components
Props are accessed through this.props. TypeScript users pass the props type as a generic argument to Component:
import { Component } from 'react'
interface GreetingProps {
name: string
age?: number
}
class Greeting extends Component<GreetingProps> {
render() {
const { name, age } = this.props
return (
<div>
<h1>Hello, {name}!</h1>
{age !== undefined && <p>Age: {age}</p>}
</div>
)
}
}
// Usage — same as a function component
<Greeting name="Alice" age={30} />State in Class Components
Class component state lives in this.state and is updated via this.setState(). You must not mutate this.state directly — React will not detect the change and the UI will not update.
import { Component } from 'react'
interface CounterState {
count: number
}
class Counter extends Component<{}, CounterState> {
// Initialize state — two styles:
// Style 1: class field (modern, preferred)
state: CounterState = { count: 0 }
// Style 2: constructor (older, needed if you use props to derive initial state)
constructor(props: {}) {
super(props) // MUST call super(props) before anything else
this.state = { count: 0 }
}
increment = () => {
// setState merges the object into existing state — no need to spread
this.setState({ count: this.state.count + 1 })
}
render() {
return (
<div>
<p>Count: {this.state.count}</p>
<button onClick={this.increment}>Increment</button>
</div>
)
}
}setState Is Asynchronous
this.setState() does not immediately update this.state. React batches state updates and applies them before the next render. If you need to compute the next state based on the current state, always use the functional form:
// ✗ BUGGY — this.state.count may be stale
this.setState({ count: this.state.count + 1 })
this.setState({ count: this.state.count + 1 })
// Both reads see the OLD count — you end up with count + 1, not count + 2
// ✓ CORRECT — functional form receives the guaranteed latest state
this.setState(prevState => ({ count: prevState.count + 1 }))
this.setState(prevState => ({ count: prevState.count + 1 }))
// Now correctly results in count + 2Lifecycle Methods
Class components have lifecycle methods that run at specific moments in a component's life. The three most important ones map directly to useEffect scenarios:
Lifecycle Method | When it runs | Equivalent hook pattern |
|---|---|---|
componentDidMount | After first render (component is in the DOM) | useEffect(() => { … }, []) |
componentDidUpdate | After every re-render (when props or state changed) | useEffect(() => { … }, [dep1, dep2]) |
componentWillUnmount | Just before component is removed from the DOM | useEffect(() => { return () => { … } }, []) |
import { Component } from 'react'
interface TimerState {
seconds: number
}
class Timer extends Component<{}, TimerState> {
state: TimerState = { seconds: 0 }
private intervalId: ReturnType<typeof setInterval> | null = null
componentDidMount() {
// Runs once — after the component appears in the DOM
this.intervalId = setInterval(() => {
this.setState(prev => ({ seconds: prev.seconds + 1 }))
}, 1000)
}
componentDidUpdate(prevProps: {}, prevState: TimerState) {
// Runs after every re-render — compare to avoid infinite loops
if (prevState.seconds !== this.state.seconds && this.state.seconds === 10) {
console.log('Ten seconds reached!')
}
}
componentWillUnmount() {
// Runs before removal — clean up to avoid memory leaks
if (this.intervalId) {
clearInterval(this.intervalId)
}
}
render() {
return <p>Elapsed: {this.state.seconds}s</p>
}
}The Equivalent Function Component
The Timer above written as a modern function component is significantly shorter and easier to follow — the setup and teardown logic live together in a single useEffect:
import { useState, useEffect } from 'react'
function Timer() {
const [seconds, setSeconds] = useState(0)
useEffect(() => {
// Setup — equivalent to componentDidMount
const id = setInterval(() => {
setSeconds(prev => prev + 1)
}, 1000)
// Teardown — equivalent to componentWillUnmount
return () => clearInterval(id)
}, []) // Empty array = run only on mount/unmount
useEffect(() => {
if (seconds === 10) console.log('Ten seconds reached!')
}, [seconds]) // Equivalent to componentDidUpdate with a guard
return <p>Elapsed: {seconds}s</p>
}When You Still Need Class Components
There is currently one feature only available in class components: Error Boundaries. A component that catches JavaScript errors in its subtree must be a class component (at least for now — a hook-based alternative is being developed):
import { Component, ErrorInfo, ReactNode } from 'react'
interface Props { children: ReactNode }
interface State { hasError: boolean }
class ErrorBoundary extends Component<Props, State> {
state: State = { hasError: false }
static getDerivedStateFromError(): State {
return { hasError: true }
}
componentDidCatch(error: Error, info: ErrorInfo) {
console.error('Caught error:', error, info.componentStack)
}
render() {
if (this.state.hasError) {
return <h2>Something went wrong.</h2>
}
return this.props.children
}
}Class Component Checklist
Extend
React.Component(orReact.PureComponentfor shallow-prop-equality optimization)Always call
super(props)as the first line of the constructorNever mutate
this.statedirectly — always usethis.setState()Use the functional form of
setStatewhen next state depends on current stateClean up subscriptions, timers, and event listeners in
componentWillUnmountWrap legacy class subtrees in an Error Boundary — hooks cannot catch render errors