June 5, 2025

React Hooks vs Class Components: A Beginner-Friendly Comparison

If you’re new to React or have experience with class components, you may have heard that hooks have brought significant changes to the way React applications are developed.

Early in React’s history, class components were the standard approach., class components were everywhere. Managing state, dealing with lifecycle methods like componentDidMount, and passing data using context often felt confusing and repetitive. With the introduction of hooks, React development became simpler and cleaner.

In this blog, I want to walk you through the basics of four important hooks:

  • useState
  • useEffect
  • useRef
  • useContext

For each hook, I’ll show you what code looked like before hooks (using classes) and how much easier it is with hooks. I’ll also explain the pain points and benefits from my perspective.

1. useState: Managing Local Component State

Let’s start with the most used hook - useState.

Without Hooks (Class Component)

class Counter extends React.Component {
    state = { count: 0 };
 
    increment = () => {
      this.setState({ count: this.state.count + 1 });
    };
 
    render() {
      return (
        <div>
          <h3>Count: {this.state.count}</h3>
          <button onClick={this.increment}>Increment</button>
        </div>
      );
    }
  }

With Hook: useState (Function Component)

import React, { useState } from "react";
function Counter() {
    const [count, setCount] = useState(0);
 
    return (
      <div>
        <h3>Count: {count}</h3>
        <button onClick={() => setCount(count + 1)}>Increment</button>
      </div>
    );
  }

What Was Difficult Without Hooks:
  • You had to use this.state, this.setState, and bind functions.
  • Even for small components, you needed a class.
  • State logic wasn’t reusable.

Why useState is Better:
  • Much cleaner and easier to read.
  • No more dealing with this.
  • You can manage multiple states independently in one component.

2. useEffect: Doing Side Effects (e.g. API Calls, Timers)

When your component needs to fetch data, set up a timer, or do something after render - you use useEffect.

Without Hooks (Class Component)

class Timer extends React.Component {
  state = { seconds: 0 };

  componentDidMount() {
    this.interval = setInterval(() => {
      this.setState(prev => ({ seconds: prev.seconds + 1 }));
    }, 1000);
  }

  componentWillUnmount() {
    clearInterval(this.interval);
  }

  render() {
    return <h3>Time: {this.state.seconds} seconds</h3>;
  }
}

With Hook: useEffect(Function Component)

import React, { useEffect} from "react";
function Timer() {
  const [seconds, setSeconds] = useState(0);

  useEffect(() => {
    const interval = setInterval(() => setSeconds(s => s + 1), 1000);
    return () => clearInterval(interval);
  }, []);

  return <h3>Time: {seconds} seconds</h3>;
}


What Was Difficult Without Hooks:
  • You had to spread your logic across multiple lifecycle methods.
  • It was hard to keep related logic together.
  • Cleanup code (like stopping a timer) was messy.

Why useState is Better:
  • Everything lives in one place.
  • Cleanup is easy with the return function.
  • Execution is controlled by specifying dependencies in the array.


3. useRef: Getting a Reference to a DOM Element or Persistent Value

If you’ve ever needed to directly access an input field or persist a value between renders without triggering a re-render — useRef is the way to go.

Without Hooks (Class Component)

class FocusInput extends React.Component {
  constructor() {
    super();
    this.inputRef = React.createRef();
  }

  focus = () => {
    this.inputRef.current.focus();
  };

  render() {
    return (
      <div>
        <input ref={this.inputRef} type="text" />
        <button onClick={this.focus}>Focus Input</button>
      </div>
    );
  }
}

With Hook: useRef (Function Component)

import React, { useRef} from "react";
function FocusInput() {
    const inputRef = useRef(null);
 
    return (
      <div>
        <input ref={inputRef} type="text" />
        <button onClick={() => inputRef.current.focus()}>Focus Input</button>
      </div>
    );
  }

What Was Hard Without Hooks:

  • You needed constructors and had to manage refs manually.
  • It made the component more complex.

Why useRef is Better:

  • You can declare and use it easily in functional components.
  • It’s perfect for keeping mutable values around without causing re-renders.

4. useContext: Accessing Global Data Easily

When you want to pass down data (like theme or user info) to deeply nested components, useContext is your friend.

Without Hooks (Class Component)

const ThemeContext = React.createContext('light');

class ThemedButton extends React.Component {
  static contextType = ThemeContext;

  render() {
    const theme = this.context;
    return <button style={{ background: theme === 'dark' ? '#333' : '#eee' }}>
             Theme: {theme}
           </button>;
  }
}

With Hook: useContext (Function Component)

import React, { useContext} from "react";
const ThemeContext = React.createContext('light');

function ThemedButton() {
  const theme = useContext(ThemeContext);
  return <button style={{ background: theme === 'dark' ? '#333' : '#eee' }}>
           Theme: {theme}
         </button>;
}

What Was Hard Without Hooks:

  • You had to use static contextType.
  • It wasn’t easy to use multiple contexts at once.
  • More boilerplate.

Why useContext is Better:

  • Just call useContext in any functional component.
  • Clean and simple syntax.
  • Great for global state like auth, theme, user, etc.

Final Thoughts

Hooks made React fun again. No more fighting with this, long lifecycle methods, or confusing state logic.

For those new to hooks, it is recommended to start by gradually converting class components to functional components using hooks. As familiarity with hooks increases, many developers find them to be a more efficient and maintainable approach to building React applications.

No comments:

Post a Comment