DEV Community

Cover image for React Had a Mess Problem. Here's How Hooks Fixed It.
Ebenezer
Ebenezer

Posted on

React Had a Mess Problem. Here's How Hooks Fixed It.

Here's something nobody explains when they first show you React:

For years, even experienced React developers were writing code that worked perfectly — and still felt like a tangled mess. Logic in one place. State in another. Cleanup in a third. Related things scattered across unrelated locations, like someone who organizes their house by putting all red objects in one room, regardless of what they actually are.

The problem wasn't the developers. The problem was the tool they were given.

In 2019, React 16.8 shipped something called Hooks — and it quietly changed how everyone writes React. Not because it added flashy new features. But because it finally gave developers a cleaner way to think.

Here's exactly what we'll cover:

  • Why class components got messy over time
  • How function components are fundamentally different
  • What Hooks actually are, with a real side-by-side code example

1. The Problem With Class Components

When React was young, class components were the only way to have state and lifecycle behavior in your components. You needed a class. No class, no state. That was the rule.

Here is what a typical class component looks like:

// A timer component — counts seconds since the page loaded
import React, { Component } from 'react';

class Timer extends Component {
  constructor(props) {
    super(props);
    this.state = { seconds: 0 }; // initialize state in constructor
  }

  componentDidMount() {
    // start the timer when the component appears on screen
    this.interval = setInterval(() => {
      this.setState({ seconds: this.state.seconds + 1 });
    }, 1000);
  }

  componentWillUnmount() {
    // clean up the timer when the component is removed
    clearInterval(this.interval);
  }

  render() {
    return <p>Seconds elapsed: {this.state.seconds}</p>;
  }
}

export default Timer;
Enter fullscreen mode Exit fullscreen mode

What just happened here is: the timer starts in componentDidMount and stops in componentWillUnmount. Same feature. Two completely separate methods. If you add more features — say, tracking user activity or fetching data — each one gets split across these lifecycle methods too. Your componentDidMount starts holding three different things that have nothing to do with each other.

Now imagine a component with five features. Your lifecycle methods turn into a crowded room where unrelated things share the same floor.

That was the real problem. Not the syntax. The mental model.

There were three specific pains that became impossible to ignore:

First: this was confusing. In JavaScript, this doesn't always refer to what you think it does inside class methods. Developers had to bind methods in the constructor or use arrow functions — and newcomers made this mistake constantly.

Second: sharing logic was painful. If two components needed the same behavior — say, both need to listen for window resize events — you couldn't extract that logic cleanly. The community invented workarounds called higher-order components and render props. These worked, but they created deeply nested component trees that were hard to read and debug. Developers called it "wrapper hell."

Third: related logic got split up. Setup and cleanup for the same feature lived in different methods. That made components harder to follow, harder to test, and harder to maintain over time.

React's own documentation describes it directly: classes created friction because of this, method binding, and verbosity. The team needed a better primitive.


2. Function Components — Simpler, But Once Limited

Function components were always part of React. They looked like this:

// A simple greeting — no state, just display
function Greeting(props) {
  return <h1>Hello, {props.name}</h1>;
}
Enter fullscreen mode Exit fullscreen mode

Clean. Readable. Easy to test. But before 2019, they had one hard limitation: they couldn't hold state. The moment you needed a counter, a timer, or any data that changes, you had to switch to a class. Function components were only used for "dumb" display components.

Hooks removed that limitation completely.


3. What Hooks Actually Are

Think of Hooks like plug sockets built into function components.

The component itself is the wall. Before Hooks, the wall had no sockets — you couldn't plug anything in. Classes were a workaround: a separate structure you'd build around the wall just to get sockets. Hooks added the sockets directly to the wall.

Hooks are special functions that let function components "hook into" React features — state, side effects, context, and more. The two most important ones are:

  • useState — gives your component a memory (local state)
  • useEffect — lets your component do things after rendering (like timers, API calls, subscriptions)

Here is the exact same timer component from earlier, rewritten with Hooks:

// The same timer — now as a function component with Hooks
import React, { useState, useEffect } from 'react';

function Timer() {
  const [seconds, setSeconds] = useState(0); // useState gives us state + a setter

  useEffect(() => {
    // start the timer — this runs after render
    const interval = setInterval(() => {
      setSeconds(prev => prev + 1);
    }, 1000);

    // cleanup — this runs when the component is removed
    return () => clearInterval(interval);
  }, []); // empty array means: run this effect only once, on mount

  return <p>Seconds elapsed: {seconds}</p>;
}

export default Timer;
Enter fullscreen mode Exit fullscreen mode

What just happened here is: setup and cleanup for the timer are written together, inside a single useEffect. They belong to the same feature, so they live in the same place. No this. No constructor. No separate lifecycle methods.

Same result. Completely different structure.


4. Class vs Function — A Direct Comparison

Here is the same feature written both ways, side by side:

// CLASS COMPONENT — counter with state
class Counter extends Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
    this.increment = this.increment.bind(this); // binding required
  }

  increment() {
    this.setState({ count: this.state.count + 1 });
  }

  render() {
    return (
      <div>
        <p>Count: {this.state.count}</p>
        <button onClick={this.increment}>Add 1</button>
      </div>
    );
  }
}
Enter fullscreen mode Exit fullscreen mode
// FUNCTION COMPONENT — same counter, with useState Hook
function Counter() {
  const [count, setCount] = useState(0); // one line replaces constructor + state

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Add 1</button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Both work. Both render a number that increases when you click the button. But look at how much of the class component is boilerplate — constructor, binding, this.state, this.setState. The Hook version gets straight to the point.

This is why developers adopted Hooks fast. Not because classes are bad, but because Hooks remove a lot of the noise that wasn't contributing anything.


5. Three Rules Hooks Follow

Hooks are not magic — they follow a strict internal order to work correctly. React uses the order of Hook calls to track which state belongs to which Hook across renders. Because of this, there are two rules you must follow:

Rule 1 — Only call Hooks at the top level. Never inside loops, conditions, or nested functions. Always at the top of your component.

Rule 2 — Only call Hooks inside function components or other custom Hooks. Never inside regular JavaScript functions.

Break these rules and React loses track of which state is which. You'll get bugs that are confusing to debug. Follow them and everything works predictably.


6. Why This Matters for Your React Journey

React still supports class components. They are not going anywhere. But the community has moved to function components with Hooks, and all modern React code you'll read — in job codebases, open source projects, and tutorials — is written this way.

Understanding why Hooks exist is more valuable than memorizing their syntax. The syntax you can look up. The mental model is what makes you confident when you're reading someone else's code and need to figure out what it's doing.

Hooks were not designed to add new features. They were designed to fix a structural problem — the tendency of React components to become hard to manage as they grow. They give you a way to keep related things together, remove the friction of classes, and build reusable logic that can be shared across components cleanly.


Your next step: Take the class-based Counter above and type it out by hand. Then type the Hook version. Don't copy-paste — type it. Notice what you skip. That gap is what Hooks removed.

This is Part 1 of the React From Scratch series. Next up: useState in depth — how React actually remembers things between renders.

Top comments (0)