Starting with version 16.8, React provides a way to use component and global state without the need for class components. This does not mean that Hooks are a replacement for class components, though. There are some benefits of using class components, which I will describe later in this tutorial.

First, I will lead you through handling state in both Hooks and class components, explain what custom Hooks are, and guide you through writing tests for the Hooks. Lastly, you will learn how to automate your tests using continuous integration (CI) with CircleCI.

Prerequisites

To follow along with this tutorial you will need:

Our tutorials are platform-agnostic, but use CircleCI as an example. If you don’t have a CircleCI account, sign up for a free one here.

Hooks vs class-based components

Class components are basically JavaScript Object Oriented classes with functions you can use to render React components. The advantage of using classes in React is that they contain lifecycle methods that identify when state changes and update the global state or the component state using the keyword this.state. In contrast, Hooks are used in React functional components and enable you to have components state and other React features in functional components without the need for classes. React provides a way to Hook into the global state without the class lifecycle methods for updating the global and local state of your applications.

In this section of the tutorial, you will make a counter component that uses both React Hooks and a class to increment and decrement count. Then I will show you how to initialize and update state in both.

Your first step is to clone the repository you will be working with.

Cloning the repository

On a terminal in your working directory, run these commands:

git clone https://github.com/CIRCLECI-GWP/react-class-components-to-hooks.git # Clone repository

cd react-class-components-to-hooks # Change directory to the cloned repository

After you have cloned the repository, install the dependencies and start the application. Run this command:

npm install # Install dependencies

npm start # Start the application

Once the start command has been run, the application should begin executing on the browser.

Counter application

This is a simple React class component that is created by this snippet:

// src/class.js
import React from "react";

class CounterClass extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
  }

  increment = () => {
    this.setState({
      count: this.state.count + 1,
    });
  };
  decrement = () => {
    this.setState({
      count: Math.max(this.state.count - 1),
    });
  };
  render() {
    return (
      <div className="counter">
        <h1>COUNTER</h1>
        <div className="buttons">
          <button onClick={this.increment}>Increment</button>
          <button onClick={this.decrement}>Decrement</button>
        </div>
        <p>{this.state.count}</p>
      </div>
    );
  }
}

export default CounterClass;

The constructor method of this snippet sets state to this.state and initializes the count with 0. It then defines functions that are called when the buttons are clicked. These functions update the state using the setState() method. This is the class component implementation of updating the counter application with classes. Next, review how the same is implemented using functional components and Hooks. The src/hooks.js in the cloned repository contains the Hook implementation:

// src/hooks.js
import { useState } from "react";

export default function CounterHooks() {
  const [value, setValue] = useState(0);

  const handleIncrement = () => {
    setValue(value + 1);
  };
  const handleDecrement = () => {
    setValue(value - 1);
  };
  return (
    <div className="counter">
      <h1>COUNTER</h1>
      <div className="buttons">
        <button data-testid="increment" onClick={handleIncrement}>
          Increment
        </button>
        <button data-testid="decrement" onClick={handleDecrement}>
          Decrement
        </button>
      </div>
      <p data-testid="count">{value}</p>
    </div>
  );
}

Instead of using the this.state as in the previous snippet, you can use the useState() Hook to initialize the state. The useState Hook also has the ability to share state with other components in the application, just like a class component using this.state.

These code snippets demonstrate the improved readability of the code. Not only has the complexity been removed, but you also made the functional component do only one thing - render the counter application. Now that you know what React Hooks are, why not explore the most common Hooks in React and how they are used?

useState vs useEffect Hooks

There are different Hooks you can use to perform operations in React. One of them is the useEffect() Hook. This Hook helps you handle things that are outside the realm of React such as API calls, asynchronous events, and other side effects. The structure of a simple useEffect Hook is shown in this snippet:

useEffect(() => {
  //Your code here
}, []);

The first argument expected by the useEffect Hook, is a callback function where you write the code to be executed. The second is an array [] called a dependency array. If the array is omitted, the callback function will run every time the code changes. If the array is empty, the callback function will run once. If there is a value provided, the callback function will run each time the value changes.

Note: A dependency array is an array that takes dependencies or variables and if the value changes, the callback function runs again.

Next, try using a useEffect() Hook in simple logic to log the value of count to the Chrome browser console. In this case, you want to return the value of the count, so you would add the value to the dependency array as shown in this snippet:

useEffect(() => {
  console.log(value);
}, [value]);

When the component loads and the useEffect Hook is called, the console logs the value of the count to the Chrome browser console. Every time the value of the count changes, the console logs the new value of the count.

The useState() Hook, in contrast, is a Hook used to initialize the state of the application. It takes a value as an argument and returns an array of two values. The first value is the current state and the second value is a function you can use to update the state.

Using React Hooks such as useState() and the useEffect(), you can eliminate the use of lifecycle methods like componentDidMount() and componentDidUpdate(). Instead, you can use the Hook to handle state logic.

Note: Lifecycle methods are built into React. They are used to perform operations when a certain action takes place, such as rendering, mounting, updating and unmounting. They are used in class-based components only.

Having explored some of the Hooks, you can move on to explore some of the advantages and disadvantages of using Hooks.

Advantages of Hooks

  • Hooks don’t need the this to bind the functions for the click events, and also access values in the component or global states.
  • Hooks make code cleaner and easy to read and test.
  • Hooks offer more flexibility and they can be reused, especially custom ones in multiple components.
  • With Hooks, you do not need to use lifecycle methods. Side effects can be handled by a single function.

Disadvantages of Hooks

  • It can be a challenge to get started with Hooks, especially for a new developer.
  • Every time the state changes, the component re-renders unless you use other Hooks to prevent this.

Creating custom Hooks

In the previous section, I described the advantages and disadvantages of using Hooks. In this section, I will lead you through creating a custom Hook that can be used anywhere in the counter application. Add this code snippet in the src/components/useCounter.js file:

// src/components/useCounter.js
import { useState, useEffect } from "react";

export function useCounter() {
  const [value, setValue] = useState(0);
  const [isEven, setIsEven] = useState(false);

  useEffect(() => {
    if (value % 2 === 0) {
      setIsEven(true);
    } else {
      setIsEven(false);
    }
  }, [value]);

  const handleIncrement = () => {
    setValue(value + 1);
  };
  const handleDecrement = () => {
    setValue(value - 1);
  };

  return [value, isEven, handleIncrement, handleDecrement];
}

This code snippet adds a new state value, isEven, that checks whether the value is even or not. The snippet goes on to check the count value and determine if that is even or odd. It sets isEven to true or false depending on the value.

The callback function inside the useEffect Hook uses an if - else statement to set the value of isEven. It also use a value in the dependency array to ensure that every time count changes, either as a decrement or an increment, the function will run.

The useCounter Hook returns the state values and the increment and decrement functions so that you can access them in the Hooks component.

Now that you have the custom Hook, you can use it to set and update state in the custom-hook.js file:

// src/components/custom-hook.js
import { useCounter } from "./useCounter";

export default function CounterHooks() {
  const [value, isEven, handleIncrement, handleDecrement] = useCounter();

  return (
    <div className="counter">
      <h1>COUNTER</h1>
      <div className="buttons">
        <button data-testid="increment" onClick={handleIncrement}>
          Increment
        </button>
        <button data-testid="decrement" onClick={handleDecrement}>
          Decrement
        </button>
      </div>
      <p data-testid="count">{value}</p>
      <div className={isEven ? "even" : "odd"}>{isEven ? "Even" : "Odd"}</div>
    </div>
  );
}

This code snippet uses the useCounter() Hook to set state values and also access the increment and decrement functions. It uses those functions to update the state. The isEven state value shows when the counter is even or odd depending on the counter digit displayed on the application.

Odd and Even hooks display

Now that you have had some fun with Hooks, it is time to learn how to test them.

Testing Hooks

In this section, I will describe how to write tests for the Hook component. You will be using Jest and react-testing-library, both of which were installed when you set up the cloned application.

Start by testing whether the buttons work. Add this code snippet to the App.test.js file:

// src/App.test.js
import { render, screen, fireEvent } from "@testing-library/react";
import App from "./App";

describe("Counter component test suite", () => {
  test("displays the heading", () => {
    render(<App />);
    expect(screen.getByRole("heading").textContent).toBe("COUNTER");
  });

  test("increment button works", () => {
    render(<App />);
    const count = screen.getByTestId("count");
    const incrementBtn = screen.getByTestId("increment");
    expect(count.textContent).toBe("0");
    fireEvent.click(incrementBtn);
    expect(count.textContent).toBe("1");
  });

  test("decrement button works", () => {
    render(<App />);
    const count = screen.getByTestId("count");
    const decrementBtn = screen.getByTestId("decrement");
    expect(count.textContent).toBe("0");
    fireEvent.click(decrementBtn);
    expect(count.textContent).toBe("-1");
  });
});

This snippet “clicks” the increment and decrement buttons to check whether the count value is incremented or decremented. That is asserted against the count value. Run the tests by running npm test in the terminal.

PASS  src/App.test.js (5.799 s)
  Counter component test suite
    √ displays the heading (432 ms)
    √ increment button works (77 ms)
    √ decrement button works (48 ms)

Test Suites: 1 passed, 1 total
Tests:       3 passed, 3 total
Snapshots:   0 total
Time:        12.097 s
Ran all test suites related to changed files.

In this case, the tests passed. Hurrah! The snippet shows that the react-testing-library simulates the click events from users on the application, and verifies whether the DOM state of the tests changes to what is expected in these assertions. You can now go the next section and learn how to integrate your tests with a continuous integration pipeline. For this case we will use CircleCI.

Integrating CircleCI

CircleCI is a platform that helps software teams build, test, and deploy automatically through the principle of continuous integration and continuous deployment (CI/CD).

In the root folder of your project, create a .circleci directory and add a config.yml file to it. Add this code snippet to the configuration file:

# .circleci/config.yml
version: 2.1
jobs:
  build:
    working_directory: ~/repo
    docker:
      - image: cimg/node:21.4.0
    steps:
      - checkout
      - restore_cache:
          key: dependency-cache-{{ checksum "package-lock.json" }}
      - run:
          name: install dependencies
          command: npm install
      - save_cache:
          key: dependency-cache-{{ checksum "package-lock.json" }}
          paths:
            - ./node_modules
      - run:
          name: Hooks component tests
          command: npm test
      - store_artifacts:
          path: ~/repo/class-components-to-react-hooks

Commit and push the changes to the repository. Then go to the CircleCI dashboard.

Open the Projects page, which lists all the GitHub repositories associated with your GitHub username or organization. For this tutorial, click react-class-components-to-hooks. Select Set Up Project.

Setting up project CircleCI

Select the option to use an existing configuration in the branch main.

Setting up CircleCI

On the CircleCI dashboard expand the build workflow details to verify that all the steps were a success.

Successful pipeline execution

Expand the Hooks component tests build step to verify that the Hooks tests were run successfully.

Successful tests execution

Now when you make a change to your application, CircleCI will automatically run these tests.

Conclusion

In this tutorial, you have learned about React Hooks and their role in relation to class-based components. I described the pros and the cons of using Hooks, and how to use different types of Hooks to get the same results you can with class-based components. You were able to use your knowledge about Hooks to write a custom Hook component. You used the custom Hook in the counter application and also wrote tests for it, which you automated in a CI pipeline.

I enjoyed creating this tutorial for you, and I hope you found it valuable. Until the next one, keep learning and keep building!


Waweru Mwaura is a software engineer and a life-long learner who specializes in quality engineering. He is an author at Packt and enjoys reading about engineering, finance, and technology. You can read more about him on his web profile.

Read more posts by Waweru Mwaura