React Hooks: Modern State Management

Abstract image used to represent React Hooks: Modern State Management
Image by Joe Dudeck.

Since React 16.8, Hooks have made stateful logic easier to reuse between React components. The difference between class and function components still matters in older code, but the first decision for new work is where the state belongs. useState and useReducer are mechanisms for holding state. Context distributes a value through a component tree, whilst a custom Hook reuses stateful logic. Separate calls to a custom Hook do not share that Hook's local state by themselves.


What are React Hooks?

They are fairly new, so you can be forgiven if you've not yet had much exposure to them. Basically, React Hooks are functions which allow functional components to use state and lifecycle features that were previously only available in class components. They were introduced in React 16.8 and are now considered the preferred approach for managing state in new React applications.

Some of the most commonly used Hooks include:

  • useState – Manages local component state.
  • useEffect – Handles side effects, replacing lifecycle methods.
  • useContext – Accesses context without nested consumers.

Hooks remove the need for class components almost entirely, making React applications more concise and readable.


Managing State with useState

The useState Hook allows functional components to maintain local state without needing a class.

Example Before Hooks (Class Component)

Here's an example of what you might be familiar with from managing local state in a class component:

import React, { Component } from "react";

class Counter extends Component<{}, { count: number }> {
  constructor(props: {}) {
    super(props);
    this.state = { count: 0 };
  }

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

  render() {
    return (
      <div>
        <p>Count: {this.state.count}</p>
        <button onClick={this.increment}>Increment</button>
      </div>
    );
  }
}

export default Counter;

This is a fairly simple component that uses its state to store a count which is then updated incrementally every time the button is clicked on.

Example Using useState

Here is the same component, but using useState as a functional component instead:

import React, { useState } from "react";

const Counter: React.FC = () => {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
};

export default Counter;

Why useState Is Better

As you can see from the examples above, useState (and being a functional component) is much smaller and more intuitive. This new use of useState...

  • Eliminates the need for class components.
  • More concise and readable syntax.
  • State updates are explicit and easier to understand.

Handling Side Effects with useEffect

Before Hooks, class components used lifecycle methods such as componentDidMount, componentDidUpdate and componentWillUnmount to manage side effects. useEffect instead describes how a component synchronises with an external system: React runs the setup after the relevant render, runs its returned cleanup before a changed dependency causes another setup, and runs the cleanup once more when the component unmounts.

Example Before Hooks (Class Component)

Let's take a look at a fairly simple example where a component fetches data from an API when it mounts, and then stores it in local state to render it inside a <p> element.

As a class component, this might look something like:

import React, { Component } from "react";

class FetchData extends Component<{}, { data: string }> {
  constructor(props: {}) {
    super(props);
    this.state = { data: "" };
  }

  componentDidMount() {
    fetch("https://api.example.com/data")
      .then((response) => response.json())
      .then((data) => this.setState({ data }));
  }

  render() {
    return <p>{this.state.data}</p>;
  }
}

export default FetchData;

Example Using useEffect

Here's that same component using Hooks. The effect returns a cleanup function so a response from the abandoned setup cannot update state after unmounting:

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

const FetchData: React.FC = () => {
  const [data, setData] = useState("");

  useEffect(() => {
    let ignore = false;

    fetch("https://api.example.com/data")
      .then((response) => response.json())
      .then((data) => {
        if (!ignore) setData(data);
      });

    return () => {
      ignore = true;
    };
  }, []);

  return <p>{data}</p>;
};

export default FetchData;

Why useEffect Is Better

  • Consolidates lifecycle logic in one place.
  • Avoids duplication across lifecycle methods.
  • React invokes the cleanup you return before another setup with changed dependencies, and when the component unmounts.

Comparing Hooks to Class Components

FeatureClass componentsWith hooks
State Managementthis.state & setStateuseState Hook
Side EffectsLifecycle methodsuseEffect Hook
Code ComplexityMore boilerplateConcise and readable
PerformanceDepends on rendering work and update boundaries; measure before adding PureComponent.Hooks do not prevent rerenders. Measure rendering work before adding React.memo or useMemo.

Should You Use Hooks?

As of right now, React Hooks are stable and recommended for new projects. Whilst class components still work, they are no longer necessary in most cases. Hooks provide a more modern and efficient way to manage state and side effects.

Use Hooks If:

  • You are starting a new React project.
  • You want cleaner, more readable components.
  • You want to futureproof your code as React evolves.

Use Class Components If:

  • You are maintaining an older codebase that relies on them.
  • You need error boundaries (as of right now, they still require class components although that might change).

Wrapping Up

Key Takeaways

  • Hooks were introduced in React 16.8 (earlier this year) to bring state and lifecycle features to functional components.
  • useState replaces this.state, making state management cleaner and easier.
  • useEffect replaces lifecycle methods, simplifying side effect management.
  • Hooks are now the preferred approach for state management in React, though class components are still valid for legacy applications.

State is easiest to follow when it stays close to the components that own it, with derived values calculated rather than stored twice and Effects reserved for synchronising with something outside React. A custom Hook can extract shared stateful logic, but each call has independent local state unless that state comes from a shared external source or context. A larger state tool should solve an observed coordination problem, not compensate for unclear ownership.


Need a senior engineer involved?

I can work directly in the codebase, review the architecture, or support the team through delivery when the work needs more than extra hands.