Class components in React are a legacy but still supported way to create components. Today they’re appropriate to use only in specific cases:
✅ When it’s appropriate to use class components:
❌ When NOT to use class components:
Key rule: If you’re not working with legacy code, always use functional components.
Class components were the primary way to create components in React before hooks were introduced in version 16.8. Despite functional components with hooks becoming the standard, class components are still supported and have limited application in modern development.
Class components are ES6 classes that extend React.Component and have lifecycle methods:
// Class component
import { Component } from 'react';
class UserProfile extends Component {
constructor(props) {
super(props);
this.state = { user: null, loading: true };
}
componentDidMount() {
this.fetchUser();
}
componentDidUpdate(prevProps) {
if (prevProps.userId !== this.props.userId) {
this.fetchUser();
}
}
fetchUser = async () => {
this.setState({ loading: true });
const userData = await fetch(`/api/users/${this.props.userId}`).then(r => r.json());
this.setState({ user: userData, loading: false });
}
render() {
if (this.state.loading) return <div>Loading...</div>;
if (!this.state.user) return <div>User not found</div>;
return (
<div>
<h1>{this.state.user.name}</h1>
<p>{this.state.user.email}</p>
</div>
);
}
}
The most common case for using class components is working with existing codebases:
// Example legacy component that can't be quickly rewritten
class LegacyChart extends Component {
constructor(props) {
super(props);
this.chartRef = React.createRef();
}
componentDidMount() {
// Complex integration with library requiring class component
this.chart = new ComplexChartLibrary(this.chartRef.current, this.props.config);
this.chart.render();
}
componentDidUpdate(prevProps) {
if (prevProps.data !== this.props.data) {
this.chart.updateData(this.props.data);
}
}
componentWillUnmount() {
this.chart.destroy();
}
render() {
return <div ref={this.chartRef} className="chart-container" />;
}
}
Error Boundaries are components that catch errors in child components. Currently they can only be implemented using class components:
// Error Boundary can only be implemented as a class component
class ErrorBoundary extends Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
// Static method for handling errors
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
// Logging error
console.error('Error caught by boundary:', error, errorInfo);
// Sending to monitoring system
logErrorToService(error, errorInfo);
}
render() {
if (this.state.hasError) {
return <div>Something went wrong. Please reload the page.</div>;
}
return this.props.children;
}
}
// Usage
<ErrorBoundary>
<MyComponent />
</ErrorBoundary>
Some third-party libraries require inheritance from specific classes:
// Library requiring inheritance from specific class
import { CustomComponent } from 'third-party-library';
class MyCustomComponent extends CustomComponent {
constructor(props) {
super(props);
this.state = { data: [] };
}
componentDidMount() {
super.componentDidMount();
this.fetchData();
}
fetchData = () => {
// Specific implementation for this library
}
render() {
return (
<div>
{this.state.data.map(item => (
<div key={item.id}>{item.name}</div>
))}
</div>
);
}
}
In some complex cases, class components might be more appropriate:
class ComplexComponent extends Component {
constructor(props) {
super(props);
this.state = {
data: null,
subscriptions: [],
timers: []
};
}
componentDidMount() {
this.setupSubscriptions();
this.startTimers();
this.fetchInitialData();
}
componentDidUpdate(prevProps, prevState) {
// Complex update logic
if (prevProps.filter !== this.props.filter) {
this.updateFilter();
}
if (prevState.data !== this.state.data) {
this.handleDataChange();
}
}
componentWillUnmount() {
this.cleanupSubscriptions();
this.clearTimers();
}
setupSubscriptions = () => {
// Setting up multiple subscriptions
}
startTimers = () => {
// Starting multiple timers
}
cleanupSubscriptions = () => {
// Cleaning up all subscriptions
}
clearTimers = () => {
// Cleaning up all timers
}
render() {
return <div>Complex component</div>;
}
}
For new projects, always use functional components:
// ✅ Correct - functional component
import { useState, useEffect } from 'react';
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchUser = async () => {
setLoading(true);
const userData = await fetch(`/api/users/${userId}`).then(r => r.json());
setUser(userData);
setLoading(false);
};
fetchUser();
}, [userId]);
if (loading) return <div>Loading...</div>;
if (!user) return <div>User not found</div>;
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}
For simple components, the functional approach is more concise:
// ✅ Functional component
function Button({ onClick, children }) {
return (
<button onClick={onClick}>
{children}
</button>
);
}
// ❌ Class component (redundant)
class Button extends Component {
render() {
return (
<button onClick={this.props.onClick}>
{this.props.children}
</button>
);
}
}
For components with state, hooks are simpler and clearer:
// ✅ With hooks
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Counter: {count}</p>
<button onClick={() => setCount(count + 1)}>
Increase
</button>
</div>
);
}
// ❌ With classes (more verbose)
import { Component } from 'react';
class Counter extends Component {
constructor(props) {
super(props);
this.state = { count: 0 };
}
render() {
return (
<div>
<p>Counter: {this.state.count}</p>
<button onClick={() => this.setState({
count: this.state.count + 1
})}>
Increase
</button>
</div>
);
}
}
Example of gradual migration:
// Legacy class component
class TodoList extends Component {
constructor(props) {
super(props);
this.state = { todos: [], newTodo: '' };
}
addTodo = () => {
if (this.state.newTodo.trim()) {
this.setState({
todos: [...this.state.todos, {
id: Date.now(),
text: this.state.newTodo,
completed: false
}],
newTodo: ''
});
}
}
toggleTodo = (id) => {
this.setState({
todos: this.state.todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
});
}
render() {
return (
<div>
<input
value={this.state.newTodo}
onChange={(e) => this.setState({ newTodo: e.target.value })}
onKeyPress={(e) => e.key === 'Enter' && this.addTodo()}
/>
<button onClick={this.addTodo}>Add</button>
<ul>
{this.state.todos.map(todo => (
<li
key={todo.id}
onClick={() => this.toggleTodo(todo.id)}
style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}
>
{todo.text}
</li>
))}
</ul>
</div>
);
}
}
// Modern functional component
import { useState } from 'react';
function TodoList() {
const [todos, setTodos] = useState([]);
const [newTodo, setNewTodo] = useState('');
const addTodo = () => {
if (newTodo.trim()) {
setTodos([
...todos,
{
id: Date.now(),
text: newTodo,
completed: false
}
]);
setNewTodo('');
}
};
const toggleTodo = (id) => {
setTodos(todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
));
};
return (
<div>
<input
value={newTodo}
onChange={(e) => setNewTodo(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && addTodo()}
/>
<button onClick={addTodo}>Add</button>
<ul>
{todos.map(todo => (
<li
key={todo.id}
onClick={() => toggleTodo(todo.id)}
style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}
>
{todo.text}
</li>
))}
</ul>
</div>
);
}
Sometimes both approaches can coexist in one application:
// Class component (Error Boundary)
class ErrorBoundary extends Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
console.error('Error caught:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return <div>Something went wrong!</div>;
}
return this.props.children;
}
}
// Functional components
function App() {
return (
<ErrorBoundary>
<UserProfile userId={1} />
<TodoList />
</ErrorBoundary>
);
}
// Functional component
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser(userId).then(setUser);
}, [userId]);
if (!user) return <div>Loading...</div>;
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}
// Functional component
function TodoList() {
const [todos, setTodos] = useState([]);
return (
<div>
<h2>Todo List</h2>
{todos.map(todo => (
<div key={todo.id}>{todo.text}</div>
))}
</div>
);
}
Class components in React are a legacy but still supported way to create components:
✅ When it’s appropriate to use:
❌ When NOT to use:
Key points:
Understanding when it’s appropriate to use class components will help you make more informed decisions when developing React applications and work with existing code more effectively.
Want more articles for interview preparation? Subscribe to EasyAdvice, bookmark the site, and improve every day 💪