A quick guide to testing React Hooks

The 16.8.0 version release of React meant a stable release of the React Hooks feature. React Hooks was introduced last year and got favorable reviews from the React ecosystem. It’s essentially a way to create components with features, like state, without the need for class components.

The Hooks feature is a welcome change as it solves many of the problems React devs have faced over the years. One of those problems is the case of React not having support for reusable state logic between class components. This can sometimes lead to huge components, duplicated logic in the constructor and lifecycle methods.

Inevitably, this forces us to use some complex patterns such as render props and higher order components and that can lead to complex codebases.

Hooks aim to solve all of these by enabling you to write reusable components with access to state, lifecycle methods, refs e.t.c.

Below are some of the major Hooks that will be used generally in your React apps:

useState — allows us to write pure functions with state in them

useEffect — lets us perform side effects. Side effects can be API calls, updating the DOM, subscribing to event listeners

The other Hooks that can be used in your React apps for specific edge cases include:

useReducer — An alternative to useState. Accepts a reducer of type (state, action) => newState, and returns the current state paired with a dispatch method. It is usually preferable to useState when you have complex state logic that involves multiple sub-values or when the next state depends on the previous one

useMemo — useMemo is used to return a memoized value

useCallback — The useCallback Hook is used to return a memoized callback

useImperativeMethods — useImperativeMethods customizes the instance value that is exposed to parent components when using ref

useMutationEffects — The useMutationEffect is similar to the useEffect Hook in the sense that it allows you to perform DOM mutations

useLayoutEffect — The useLayoutEffect hook is used to read layout from the DOM and synchronously re-render

Before we go on to see how to write tests for React Hooks, let’s see how to build a React app using Hooks. We’ll be building an app that shows the 2018 F1 races and the winners for each year.

The whole app can be seen and interacted with at CodeSandbox.

In the app above, we’re using the useState and useEffect Hooks. If you navigate to the index.js file, in the App function, you’ll see an instance where useState is used.

// Set the list of races to an empty array

let [races, setRaces] = useState([]);

// Set the winner for a particular year

let [winner, setWinner] = useState("");

useState returns a pair of values, that is the current state value and a function that lets you update it. It can be initialized with any type of value (string, array e.t.c) as opposed to state in classes where it had to be an object.

The other Hook that’s in use here is the useEffect Hook. The useEffect Hook adds the ability to perform side effects from a function component. It essentially allows you to perform operations you’d usually carry out in the componentDidMount, componentDidUpdate, and componentWillUnmount lifecycles.

In the app, we’re using the useEffect Hook to make API calls and fetch the F1 races data and then using the setRaces and setWinner functions to set their respective values into the state.

That’s just an example of how Hooks can be used in combination to build an app. We use the useEffect Hook to fetch data from some source and the useState to set the data gotten into a state.

Jest and Enzyme are tools used for testing React apps. Jest is a JavaScript testing framework used to test JavaScript apps and Enzyme is a JavaScript testing utility for React that makes it easier to assert, manipulate, and traverse your React Components’ output.

They are probably the go-to testing tools for React, so we’ll see if they can be used to test React Hooks. To do that, I’ve created an app on CodeSandbox that we’ll use for our test suites. You can follow along by forking the app on CodeSandbox.

Navigate to the tests folder to see the hooktest.js file that contains the test suite.

import React from "react";

import ReactDOM from "react-dom";

import App from "../index";

it("renders without crashing", () => {

const div = document.createElement("div");

ReactDOM.render(<App />, div);

ReactDOM.unmountComponentAtNode(div);

});

We’ll first write a test to see if the app renders without crashing.

Next up, we’ll try using the Enzyme testing library to test React Hooks. To use Enzyme we’ll need to install the following dependencies to the CodeSandbox app:

Navigate to the tests folder to see the hooktest.js file that contains the test suite.

In the hooktest.js file, an additional test block is added. We are testing using the shallow method imported from Enzyme. The shallow method or rendering is used to test components as a unit. It is a simulated render of a component tree that does not require a DOM.

The error above means that Hooks are not yet supported in Enzyme as seen in this issue here.

As a result, we cannot use Enzyme to carry out component tests for React Hooks. So what can be used?

react-testing-library is a very light-weight solution for testing React components. It extends upon react-dom and react-dom/test-utils to provide light utility functions. It encourages you to write tests that closely resemble how your react components are used.

Let’s see an example of writing tests for Hooks using react-testing-library.

In the app above, three types of Hooks are in use, useState, useEffect, useRef, and we’ll be writing tests for all of them.

In addition to the useState example in which we’re incrementing and decrementing a count, we’ve also added two more examples.

For the useRef Hook implementation, we’re essentially creating a ref instance using useRef and setting it to an input field and that would mean that the input’s value can now be accessible through the ref.

The useEffect Hook implementation is essentially setting the value of the name state to the localStorage.

Let’s go ahead and write tests for all of the implementation above. We’ll be writing the test for the following:

The initial count state is 0

The increment and decrement buttons work

Submitting a name via the input field changes the value of the name state

The name state is saved in the localStorage

Navigate to the tests folder to see the hooktest.js file that contains the test suite and the import line of code below.

render — this will help render our component. It renders into a container which is appended to document.body

getByTestId — this fetches a DOM element by data-testid

fireEvent— this is used to "fire" DOM events. It attaches an event handler on the document and handles some DOM events via event delegation e.g. clicking on a button

rerender — this is used to simulate a page reload

Next, add the test suite below in the hooktest.js file.

// hooktest.js

it("App loads with initial state of 0", () => {

const { container } = render(<App />);

const countValue = getByTestId(container, "countvalue");

expect(countValue.textContent).toBe("0");

});

The test checks that if the initial count state is set to 0 by first fetching the element with the getByTestId helper. It then checks if the content is 0 using the expect() and toBe() functions.

Next, we’ll write the test to see if the increment and decrement buttons work.

// hooktest.js

it("Increment and decrement buttons work", () => {

const { container } = render(<App />);

const countValue = getByTestId(container, "countvalue");

const increment = getByTestId(container, "incrementButton");

const decrement = getByTestId(container, "decrementButton");

expect(countValue.textContent).toBe("0");

fireEvent.click(increment);

expect(countValue.textContent).toBe("1");

fireEvent.click(decrement);

expect(countValue.textContent).toBe("0");

});

In the test above, The test checks that if the onButton is clicked on, the state is set to 1 and when the offButton is clicked on, the state is set to 1.

For the next step, we’ll write a test to assert if submitting a name via the input field actually changes the value of the name state and that it’s successfully saved to the localStorage.

// hooktest.js

it("Submitting a name via the input field changes the name state value", () => {

const { container, rerender } = render(<App />);

const nameValue = getByTestId(container, "namevalue");

const inputName = getByTestId(container, "inputName");

const submitButton = getByTestId(container, "submitRefButton");

const newName = "Ben";

fireEvent.change(inputName, { target: { value: newName } });

fireEvent.click(submitButton);

expect(nameValue.textContent).toEqual(newName);

rerender(<App />);

expect(window.localStorage.getItem("name")).toBe(newName);

});

In the test assertion above, the fireEvent.change method is used to enter a value into the input field, after which the submit button is clicked on.

The test then checks if the value of the ref after the button was clicked is equal to the newName. Finally, using the rerender method, a reload of the app is simulated and there’s a check to see if name set previously was stored to the localStorage.

In this article, we’ve seen how to write tests for React Hooks and React components using the react-testing-library. We also went through a short primer on how to use React Hooks.

LogRocket is a frontend logging tool that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single page apps.