React hooks vs class components

Managing state in React is an important aspect of building interactive applications. For a long time, class components were the only way to build components that had their own state. Functional components were considered stateless, or presentational, components.

However, all this was turned on its heads when React 16.8 introduced hooks. Hooks introduce new APIs like useState, useEffect, useContext, and useReducer, which can be used to manage state, handle side effects, and share data between components. These APIs are designed to be composable and can be used together to create complex stateful logic in a concise and readable way.

In this article, we will briefly cover what state is and walk through the differences between react hooks and class components.

What is state?

Very briefly, state is data that can change and trigger a re-render of a component. It allows components to manage data and have dynamic behavior. Compared to stateless, or purely presentational, components, components that have state can react to user actions and other events.

Hooks vs class components

There are some major differences between hooks and class components. We will talk about each one below.

Component boilerplate code

When writing class components, there is a lot of setup code even just to get started. This is not the case when using hooks. Let’s look at a very simple counter component to see the difference.

That is a lot of lines! To set up a class component, you need quite a bit of boilerplate code, which include the constructor() and super() methods. Now let’s look at what the same component using React hooks looks like.

This is so much cleaner! Hooks improves readability by reducing boilerplate code. Functional components written with hooks are typically shorter and easier to understand than class components. They also allow us to do away with the constructor() and super() methods of the class component and simplify the state declaration to one useState() call.

No need to worry about “this”

One of the most common challenges in JavaScript is the this keyword. Depending on how and where it is used, this can behave quite differently. We won’t go into too much detail here, but you can read this in-depth article about the different behaviors.

As we can see in the earlier example, the class component uses this in many places. However, with hooks, we no longer need it anymore, making building components less confusing!

Method binding

Method binding is related to this. When the click handler is invoked, the execution context is different from where it was defined. Because of this, the method loses what this should be and cannot find the setState() function.

Try it yourself! In the boilerplate example above, remove .bind(this) from the onClick prop, click the button, and check the console. You’ll see an error that says:

Cannot read properties of undefined (reading 'setState')

Using hooks, we don’t need to bind methods anymore. As we don’t use the this keyword, it greatly simplifies passing methods to children.

Lifecycle methods

Lifecycle methods in React are methods that are called automatically by React at specific points during a component’s lifecycle. They provide a way to hook into and control the behavior of a component during different stages of its existence, such as when it is mounted, updated, or unmounted.

The useEffect hook allows us to perform side effects, much like lifecycle methods, based on the component’s props or state. However, in contrast to lifecycle methods, useEffect greatly simplifies the logic and code required.

Let’s look at a simple example. Here is what using lifecycle methods would look like:

class ExampleComponent extends React.Component {
  ...

  componentDidMount() {
    console.log('Component mounted!');
    this.setState({ count: this.state.count + 1 });
  }

  componentDidUpdate(prevProps, prevState) {
    if (this.state.count !== prevState.count) {
      console.log('Count updated!');
    }
  }

  componentWillUnmount() {
    console.log('Component unmounted!');
  }

  ...
}

Here is what the same lifecycle methods would look like using hooks:

const ExampleComponent = (props) => {
  ...

  useEffect(() => {
    console.log('Component mounted!');
    setCount(count + 1);

    return () => {
      console.log('Component unmounted!');
    };
  }, []);

  useEffect(() => {
    console.log('Count updated!');
  }, [count]);

  ...
};

As we can see from the example above, lifecycle methods are more descriptive compared to the useEffect hook. Using hooks removes some of the verbosity, which can help developers focus on what’s important, the setup and dependencies. However, it does hide some of the logic under the hood, which can be a bit confusing to newer developers.

The main thing with useEffect is it is less about the lifecycle of a component and more about synchronizing when dependencies change. This can be easier to understand rather than trying to think about the multiple points in a component’s lifecycle.

So are class components useless now?

Not necessarily. Class components have been around for a long time and still exist in many older codebases. There is also a sea of knowledge and experience in developer forums and communities, which can be especially helpful when needing support. Classes are still a valid way of building components, and don’t seem to be going away anytime soon.

However, hooks are quickly gaining traction. They are looking like the simpler approach to building stateful functional components. That, combined with less boilerplate and not needing to use this, hooks could be here to stay.