React Hooks Changed How I Think About State

May 29, 2026

When React introduced hooks in 16.8, I was skeptical. "Why rewrite perfectly good class components?" But after building a few projects with hooks, I realized they weren't just a new API — they were a completely different mental model.

The Class Component Problem

Before hooks, managing state meant:

  • Converting functional components to class components when you needed state
  • Dealing with this binding issues
  • Splitting related logic across componentDidMount, componentDidUpdate, and componentWillUnmount
  • Sharing stateful logic via Higher-Order Components (HOCs) or render props, which led to "wrapper hell"
class Timer extends React.Component {
  constructor(props) {
    super(props)
    this.state = { seconds: 0 }
  }

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

  componentWillUnmount() {
    clearInterval(this.interval)
  }

  render() {
    return <div>Time: {this.state.seconds}s</div>
  }
}

The Hooks Solution

With hooks, the same component becomes:

function Timer() {
  const [seconds, setSeconds] = useState(0)

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

  return <div>Time: {seconds}s</div>
}

Everything related to the timer is now in one place. Setup and cleanup are colocated. No this confusion.

Key Insights

1. State is Just Data

useState makes it clear: state is just a variable that triggers a re-render when it changes. That's it.

2. Effects are Separate from Rendering

useEffect separates side effects from the render function. Your component returns JSX. Effects handle the rest.

3. Custom Hooks are the Secret Weapon

You can extract stateful logic into custom hooks and reuse it anywhere:

function useTimer(initialSeconds = 0) {
  const [seconds, setSeconds] = useState(initialSeconds)
  const [isRunning, setIsRunning] = useState(false)

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

  return { seconds, isRunning, setIsRunning }
}

Now any component can have a timer:

function Stopwatch() {
  const { seconds, isRunning, setIsRunning } = useTimer()
  return (
    <div>
      <p>Time: {seconds}s</p>
      <button onClick={()=> setIsRunning(!isRunning)}>
        {isRunning ? 'Pause' : 'Start'}
      </button>
    </div>
  )
}

When to Use Each Hook

  • useState — for simple local state (counters, toggles, form inputs)
  • useEffect — for side effects (data fetching, subscriptions, timers)
  • useReducer — for complex state logic (multiple sub-values, state transitions)
  • useContext — for passing data through the component tree without props
  • useMemo / useCallback — for performance optimization (only when needed)
  • useRef — for accessing DOM elements or persisting values across renders

Common Mistakes

1. Overusing useEffect

Not every side effect needs useEffect. Event handlers are often better:

// ❌ Unnecessary effect
useEffect(() => {
  if (buttonClicked) {
    handleSubmit()
  }
}, [buttonClicked])

// ✅ Use event handler
<button onClick={handleSubmit}>Submit</button>

2. Missing Dependencies

Always include all dependencies in the dependency array. Use ESLint's react-hooks/exhaustive-deps rule.

3. Using useMemo Too Early

Don't optimize prematurely. Most computations are fast enough without useMemo.

The Mental Shift

Hooks teach you to think in terms of what your component needs, not how it gets it. You compose behavior from hooks instead of inheriting from base classes.

Once you internalize this, you'll never want to write class components again.

Takeaway

If you're still writing class components, give hooks a real try. Build a project from scratch with them. The initial learning curve is worth it — your code will be cleaner, more reusable, and easier to test.