DEV Community

Timevolt
Timevolt

Posted on

React Hooks: The Matrix of useState and useEffect

The Quest Begins (The "Why")

I still remember the first time I tried to make a component “remember” something. I had a simple counter that needed to increment every time a button was clicked, and I wanted to fetch some data when the component showed up. My first instinct? Reach for class components, sprinkle in this.state, and call componentDidMount and componentDidUpdate like I was casting spells from an old grimoire. The code worked, but it felt clunky—lots of boilerplate, weird this bindings, and lifecycle methods scattered everywhere.

Every time I added a new piece of state, I had to duplicate the same pattern in componentDidUpdate. When I finally tried to split concerns—like separating a timer from a data fetch—I ended up with a tangled mess that looked more like a spaghetti western than a clean UI. I kept asking myself: Is there a better way?

That’s when I stumbled upon Hooks. The promise was simple: useState for local state, useEffect for side effects, and everything could live inside a functional component. I was skeptical at first—could a couple of functions really replace the whole lifecycle? But after a few hours of tinkering, I felt like Neo finally seeing the code behind the Matrix.

The Revelation (The Insight)

The magic of Hooks isn’t that they’re new; it’s that they let you group related logic together instead of forcing you to split it across mount, update, and unmount callbacks.

  • useState gives you a setter function that React will call to schedule a re‑render. It’s not magic; it’s just a closure that holds the current value and a dispatcher.
  • useEffect runs after every render by default, but you can tell React to skip it if certain values haven’t changed. Think of it as a “watch‑tower” that looks at a list of dependencies and decides whether to sound the alarm.

The real “aha!” moment came when I realized I could have multiple useState and useEffect calls in the same component, each responsible for its own concern. No more giant componentDidUpdate with a dozen if (prevState.x !== state.x) checks. Instead, I could write:

function Counter() {
  const [count, setCount] = useState(0);
  const [data, setData] = useState(null);

  useEffect(() => {
    document.title = `You clicked ${count} times`;
  }, [count]); // runs only when count changes

  useEffect(() => {
    fetchData().then(setData);
  }, []); // runs once on mount (empty deps)
Enter fullscreen mode Exit fullscreen mode

Each effect is isolated, each piece of state is explicit, and the component reads like a short story rather than a tangled novel.

Wielding the Power (Code & Examples)

The Struggle: Classic Class Component

class OldCounter extends React.Component {
  state = { count: 0, data: null };

  componentDidMount() {
    this.fetchData();
  }

  componentDidUpdate(prevProps, prevState) {
    if (prevState.count !== this.state.count) {
      document.title = `You clicked ${this.state.count} times`;
    }
  }

  fetchData = () => {
    fetch('/api/data')
      .then(res => res.json())
      .then(data => this.setState({ data }));
  };

  render() {
    return (
      <div>
        <p>Count: {this.state.count}</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          Increment
        </button>
        {this.data && <p>Data: {this.data.value}</p>}
      </div>
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Look at all the places we had to remember to update the title or refetch data. Miss a dependency, and you get stale UI or extra requests.

The Victory: Hooks‑Powered Functional Component

import React, { useState, useEffect } from 'react';

function NewCounter() {
  const [count, setCount] = useState(0);
  const [data, setData] = useState(null);

  // Effect 1: keep the title in sync with count
  useEffect(() => {
    document.title = `You clicked ${count} times`;
  }, [count]); // <-- runs only when count changes

  // Effect 2: fetch data once on mount
  useEffect(() => {
    async function load() {
      const res = await fetch('/api/data');
      const json = await res.json();
      setData(json);
    }
    load();
  }, []); // empty deps = run once

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(c => c + 1)}>
        Increment
      </button>
      {data && <p>Data: {data.value}</p>}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Notice how each concern lives next to the code that needs it. The title effect only watches count. The data fetch watches nothing—it runs once. If I ever need another piece of state, say a loading spinner, I just add another useState and maybe another useEffect with its own deps. No more hunting through lifecycle methods.

Common Traps (The “Bosses” to Avoid)

  1. Forgot the dependency array – If you leave out the second argument to useEffect, it runs after every render. That can lead to infinite loops (e.g., setting state inside the effect without a condition) or wasted work.
  2. Stale closures – When you reference a variable from the render inside an effect’s callback, make sure it’s in the dependency array, or use the functional updater form (setState(s => s + 1)) to avoid capturing an old value.

Both are easy to slip into, but once you internalize the rule—list everything the effect reads—they become second nature.

Why This New Power Matters

With useState and useEffect in your toolkit, components become self‑contained narratives. You can read a component top‑to‑bottom and see exactly what state it holds and what side effects it triggers. This makes:

  • Debugging faster – No more jumping between componentDidMount, componentDidUpdate, and componentWillUnmount.
  • Testing easier – Pure functions with predictable outputs.
  • Reusability higher – Extract custom hooks (useFetch, useForm) and share logic across components without render props or higher‑order components.

In short, you stop fighting React’s lifecycle and start describing what your UI should do, letting React handle the when.


Your Turn

Try refactoring a small class component you’ve written recently into a functional component using useState and useEffect. Pay special attention to the dependency arrays—treat them like the checklist a pilot runs before takeoff. When you see the component behave exactly as before but with far less noise, you’ll know you’ve leveled up.

What’s the first piece of state you’ll move to a hook? Share your before/after snippets in the comments—I’d love to see your quest logs! Happy coding!

Top comments (0)