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.

Introduction to React Hooks

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.

Types of Hooks

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

useContext — allows us to write pure functions with context in them

useRef — allows us to write pure functions that return a mutable ref object

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

. Accepts a reducer of type , and returns the current state paired with a method. It is usually preferable to 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.

React Hooks Example React Hooks Example by yomete using react, react-awesome-styled-grid, react-dom, react-scripts, styled-components

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.

// On initial render of component, fetch data from API. useEffect(() => { fetch(`https://ergast.com/api/f1/2018/results/1.json`) .then(response => response.json()) .then(data => { setRaces(data.MRData.RaceTable.Races); }); fetch(`https://ergast.com/api/f1/2018/driverStandings.json`) .then(response => response.json()) .then(data => { let raceWinner = data.MRData.StandingsTable.StandingsLists[0].DriverStandings[0].Driver.familyName + " " + data.MRData.StandingsTable.StandingsLists[0].DriverStandings[0].Driver.givenName; setWinner(raceWinner); }); }, []);

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.

Testing React Hooks

Can we use Jest or Enzyme?

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:

Testing React Hooks Testing React Hooks by yomete using enzyme, enzyme-adapter-react-16, react, react-dom, react-scripts

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.

We get the error below when we try to test using Enzyme.

Hooks can only be called inside the body of a function component. (https://fb.me/react-invalid-hook-call)

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?

Introducing react-testing-library

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.

Testing React Hooks Testing React Hooks by yomete using enzyme, enzyme-adapter-react-16, react, react-dom, react-scripts

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

state is 0 The increment and decrement buttons work

and buttons work Submitting a name via the input field changes the value of the name state

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.

// hooktest.js import { render, fireEvent, getByTestId} from "react-testing-library";

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

— this will help render our component. It renders into a container which is appended to getByTestId — this fetches a DOM element by data-testid

— this fetches a DOM element by 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

— this is used to “fire” DOM events. It attaches an event handler on the 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.

Implementing Redux in your app? Track Redux state and actions with LogRocket Debugging React applications can be difficult, especially when there is complex state. If you’re interested in monitoring and tracking Redux state for all of your users in production, try LogRocket. https://logrocket.com/signup/ LogRocket is like a DVR for web apps, recording literally everything that happens on your site. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores. Modernize how you debug your React apps – Start monitoring for free.

Conclusion

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.

If you have any questions or comments, you can share them below.