Class vs. Functional Components in React

Function components and React Hooks are the sensible default for most new React work, but that does not make class components irrelevant. Existing applications still contain them, and class‑based error boundaries remain part of React's API. The decision is therefore less about declaring a winner and more about understanding the code you are building or maintaining.
What are Class Components?
Class components are ES6 classes that extend React.Component. They allow you to manage state and lifecycle methods within a single component.
An Example of a Class Component:
import React, { Component } from "react";
class Counter extends Component<{ initialCount: number }, { count: number }> {
constructor(props: { initialCount: number }) {
super(props);
this.state = { count: props.initialCount };
}
increment = () => {
this.setState((prevState) => ({ count: prevState.count + 1 }));
};
render() {
return (
<div>
<p>Count: {this.state.count}</p>
<button onClick={this.increment}>Increment</button>
</div>
);
}
}
export default Counter;Here we have a fairly basic example of a class component that manages its own state using this.state and updates it with this.setState(). It renders a button which increments the count when clicked.
Key Characteristics of Class Components
- Must extend
React.ComponentorReact.PureComponent. - Use
this.stateto manage local state. - Can use lifecycle methods like
componentDidMountandcomponentDidUpdate. - Require binding for event handlers unless using arrow functions.
What are Function Components?
Function components are simpler and are defined as functions that return JSX. Before React Hooks, they were limited to stateless components, but Hooks have enabled them to handle state and side effects in much the same way (albeit simpler) than a class component can.
Example of a Functional Component
import React, { useState } from "react";
const Counter: React.FC<{ initialCount: number }> = ({ initialCount }) => {
const [count, setCount] = useState(initialCount);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
};
export default Counter;This is basically the same component as we discussed above, except that it is a function component, using the useState Hook to manage its own state. It also renders a button that increments the count when clicked.
Key Characteristics of Function Components
- Defined as functions that return JSX.
- Uses
useState,useEffect, and other Hooks for state and lifecycle behaviour. - Does not require
thiskeyword. - More concise and easier to read.
Comparing Class and Function Components
| Feature | Class Components | Function Components |
|---|---|---|
| State Management | this.state and setState | useState Hook |
| Lifecycle Methods | componentDidMount, etc. | useEffect Hook |
| Performance | No inherent disadvantage | No inherent advantage |
| Code Readability | More boilerplate | More concise |
| Best for Complex Logic | Yes | Now possible with hooks |
Error Boundaries in React
Error boundaries can show a fallback when a descendant throws during rendering, in a constructor or in a lifecycle method. They do not catch ordinary event‑handler errors, asynchronous callbacks, server‑rendering errors or errors in the boundary itself. Those failures need handling where they occur; an error boundary does not replace that work.
An Example of an Error Boundary Component
To offer a more concrete example:
import React, { Component, ErrorInfo } from "react";
class ErrorBoundary extends Component<{ children: React.ReactNode }, { hasError: boolean }> {
constructor(props: { children: React.ReactNode }) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(): { hasError: boolean } {
return { hasError: true };
}
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error("Caught error:", error, errorInfo);
}
render() {
if (this.state.hasError) {
return <h2>Something went wrong.</h2>;
}
return this.props.children;
}
}
export default ErrorBoundary;Here, ErrorBoundary displays a fallback for the descendant rendering and lifecycle errors described above. As of now, only class components can be used as error boundaries, which is one of the (very) few remaining reasons to use class components in React rather than their functional counterparts.
When to Use Class vs. Function Components
Use Class Components When:
- You're maintaining an older React codebase that already uses classes.
- You need error boundaries (currently, only class components can be error boundaries).
Use Function Components When:
- You want simpler, more readable code.
- You want to compose stateful logic through Hooks; render frequency still depends on parents, state, context and memoisation.
- You're using modern React best practices with Hooks.
- You want to ensure easier future maintenance, as React's development focus is now on function components.
Should You Use Class Components in New Projects?
Function components are a sensible default for the vast majority of new projects because Hooks compose stateful logic without class syntax. That choice does not guarantee better performance or fewer renders. Class error boundaries remain a framework‑level reason to retain classes, alongside maintaining existing class‑based code.
Wrapping Up
With the introduction of Hooks, function components offer nearly all the capabilities previously associated with classes and often allow more concise composition. The syntax itself is not a performance optimisation. Function components are the default for new work, whilst classes remain relevant in existing codebases and for class‑based error boundaries.
Key Takeaways
- Class components use
this.stateand lifecycle methods, making them more verbose. - Function components with Hooks can manage state and side effects in a simpler way.
- Function components are now the preferred approach for most new React projects.
- Understanding both helps when working with legacy React applications.
Function components are the normal starting point for new React work. Class components remain supported, still make sense in existing class‑based code, and remain necessary for an error boundary that implements componentDidCatch because React has no direct function‑component equivalent. Neither syntax is a performance optimisation by itself.