When is it appropriate to use class components today?

👨‍💻 Frontend Developer 🟠 May come up 🎚️ Medium
#React

Brief Answer

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:

  • Supporting legacy code
  • Working with libraries requiring inheritance from Component
  • Specific patterns where Error Boundaries are needed
  • Integration with libraries that don’t support hooks

When NOT to use class components:

  • New projects - always use functional components
  • Simple components - hooks are more concise
  • Stateful components - useState/useReducer are simpler

Key rule: If you’re not working with legacy code, always use functional components.


Full Answer

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.

What are class components

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>
    );
  }
}

When it’s appropriate to use class components

1. Supporting legacy code

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" />;
  }
}

2. Error Boundaries

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>

3. Integration with libraries requiring inheritance

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>
    );
  }
}

4. Components with multiple lifecycle methods

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>;
  }
}

When NOT to use class components

1. New projects

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>
  );
}

2. Simple components

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>
    );
  }
}

3. Stateful components

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>
    );
  }
}

Practical Examples

1. Migration from class components

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>
  );
}

2. Coexistence of class and functional components

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>
  );
}

Summary

Class components in React are a legacy but still supported way to create components:

When it’s appropriate to use:

  • Supporting legacy code
  • Error Boundaries (not yet available in functional components)
  • Integration with libraries requiring inheritance
  • Specific patterns where classes are necessary

When NOT to use:

  • New projects - always use functional components
  • Simple components - hooks are more concise
  • Stateful components - useState/useReducer are simpler
  • Most cases - functional components are preferable

Key points:

  • Class components are still supported but not recommended
  • Error Boundaries are currently only available in class components
  • For new projects, always use functional components with hooks
  • When working with legacy code, gradually migrate to functional components

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 💪