Editor’s note: This article was last updated by Isaac Okoro on 18 December 2023 to add a more in-depth overview and comparison of Jest, Enzyme, and React Testing Library, discuss solutions to common challenges encountered while testing React Hooks, and provide updated information related to the most recent React v18.
The stable release of React Hooks in React v16.8.0 was met with favorable reviews from the community. Along with this stable release came the need to test React Hooks efficiently.
The importance of testing in the frontend can’t be stressed enough. Every team and company employs test-driven development to instill confidence in their software. This article provides a practical guide to testing React Hooks using tools such as React Testing Library, Jest, and Enzyme.
The Replay is a weekly newsletter for dev and engineering leaders.
Delivered once a week, it's your curated guide to the most important conversations around frontend dev, emerging AI tools, and the state of modern software.
React Hooks essentially provide a way to create components with features, like state, without the need for class components.
Standard and custom React Hooks solve many of the problems React devs faced over the years. These problems inevitably forced us to use some complex patterns as workarounds, such as render props and higher-order components, which can lead to complex codebases.
For example, before introducing Hooks, React had no support for reusable state logic between class components. This sometimes led to huge components, duplicated logic in the constructor, and the need to use lifecycle methods.
Hooks aim to solve all of these by enabling you to write reusable components with access to state, lifecycle methods, and refs.
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.
You can see and interact with the whole app via 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.
Meanwhile, 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 lifecycle methods:
// 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 race data, 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 fetched data into a state.
Jest is a JavaScript testing framework with a focus on simplicity that allows you to write tests for your JavaScript applications easily. It also works for testing TypeScript, React, React Native, and Vue applications.
Jest comes with an out-of-the-box configuration for JavaScript files and an elegant API, which is used to create isolated tests, snapshot comparisons, test mocking, test coverage, and much more.
React Testing Library and Enzyme are both testing utilities that provide methods for allowing you to access DOM elements. However, they don’t handle assertions, which check whether a certain condition is true and throw an error if it’s not.
To handle assertions, you need a test runner and framework. That’s where Jest comes in. Jest acts like a test runner that identifies tests, executes them, and decides whether they passed or failed. It also provides functions for assertions, code implementation, and test suites.
Jest is considered the de facto testing framework for React applications, making it a popular choice within the React ecosystem. Its ease of use, extensive features, and active community contribute to its widespread adoption for testing.
Enzyme is a JavaScript testing utility for React that makes it easier to assert, manipulate, and traverse the output of your React components. It also provides a set of convenient methods to work with React components, enabling developers to write tests for their React applications more effectively.
Some notable features of Enzyme include:
Let’s see how we can use Enzyme to test React Hooks.
To start, let’s create a project using Create React App as follows:
npx create-react-app my-app cd my-app
Next, we’ll install the Enzyme test library along with a React adapter as follows:
npm i --save-dev enzyme enzyme-adapter-react-16
Now, create a file called setupTests.js in the src folder. Add the following below snippet to configure Enzyme’s adapter:
import Enzyme from "enzyme";
import Adapter from "enzyme-adapter-react-16";
Enzyme.configure({ adapter: new Adapter() });
The code in the setupTests.js file gets executed before our test is executed.
useState Hook with EnzymeTo test the useState Hook, let’s update the app.js file with the following:
import React from "react";
const App= () => {
const [name, setName] = React.useState("");
return (
<form>
<div className="row">
<div className="col-md-6">
<input
type="text"
placeholder="Enter your name"
className="input"
onChange={(e) => {
setName(e.target.value);
}}
/>
</div>
</div>
<div className="row">
<div className="col-md-6">
<button
type="submit"
className="btn btn-primary"
>
Add Name
</button>
</div>
</div>
</form>
);
};
export default App;
Here, we have a basic input field and a button element. Notice how we used React.useState() instead of useState(). We need this to support mocking the useState Hook in our Enzyme test:
import React from "react";
import { shallow } from "enzyme";
import App from "./App";
const setState = jest.fn();
const useStateSpy = jest.spyOn(React, "useState");
useStateSpy.mockImplementation((initialState) => [initialState, setState]);
const wrapper = shallow(<App />);
Here, we’ve successfully mocked the useState Hook and we can proceed with testing for the state update on input change as follows:
it("should update state on input change", () => {
const newInputValue = "React is Awesome";
wrapper
.find(".input")
.simulate("change", { target: { value: newInputValue } });
expect(setState).toHaveBeenCalledWith(newInputValue);
});
Enzyme supports React Hooks, although there are some downsides in .shallow() due to upstream issues in React’s shallow renderer. With React shallow renderer, useEffect() and useLayoutEffect() don’t get called.
React Testing Library is a lightweight 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.
React Testing Library’s main goal is to boost developers’ confidence in their tests by testing components in the way a user would use them. It’s already installed in CRA, making it a readily available — and therefore commonly used — test library for React.
Let’s see an example of writing tests for Hooks using React Testing Library.
In the app above, we’re using three types of Hooks — useState, useEffect, and useRef. We’ll be writing tests for all of them.
For the useRef Hook implementation, we’re essentially creating a ref instance using useRef and setting it to an input field. This means we can now access the input’s value through the ref. Meanwhile, the useEffect Hook test essentially sets the value of the name state to the localStorage.
Let’s go ahead and write tests for all of the implementations above. We’ll be writing tests to check that:
count state is 0increment and decrement buttons workname statename state is saved in the localStorageNavigate 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";
In this line of code:
render will help render our component. It renders into a container that is appended to document.bodygetByTestId fetches a DOM element by data-TestIdfireEvent 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 a buttonNext, 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 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, we are checking that the state is set to 1 if the onButton is clicked and that the state is set to 0 when the offButton is clicked.
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, as well as to check 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, we used the fireEvent.change method to enter a value into the input field, after which we fired an event to click on the submitButton.
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, we simulate a reload of the app and check to see if the name set previously was stored to the localStorage.
To implement tests for asynchronous Hooks, we can use the waitForNextUpdate function from the React Hooks Testing Library.
Async methods return promises, so be sure to call them with await or .then. The React Hooks Testing Library provides the waitFor method for testing async Hooks.
The async Hook that we’ll test accepts an API URL as a parameter, makes an asynchronous request with Axios, and returns a response object.
Create a useFetchData.js file in the src folder and add the following:
import axios from "axios"
const endpoint = "https://jsonplaceholder.typicode.com/posts/1"
export default function Endpoint() {
const [data, setData] = React.useState({
state: "LOADING",
error: "",
data: []
})
const fetchData = async () => {
try {
const result = await axios.get(endpoint)
setData({
state: "SUCCESS",
error: "",
data: result.data // Access the data directly
})
} catch (err) {
setData({
data: [],
state: "ERROR",
error: err
})
}
}
React.useEffect(() => {
fetchData()
}, [])
return data
}
Now, let’s test our async Hook. Create a useFetchData.test.js file in the src folder and add the following:
import { renderHook } from "@testing-library/react"
import useFetchData from "./useFetchData"
import axios from "axios"
import { act } from "react-dom/test-utils"
jest.mock('axios');
const useApiMockData = [
{ id: 1, name: "Leanne Graham" },
{ id: 2, name: "Ervin Howell" }
]
Here, we use a jest.mock method to mock Axios for testing async actions. Also, we’ve mocked some typical response data for our test.
Next, we’ll write a test for successful and failed API requests:
describe("useFetchData Hook", () => {
it("initial and success state", async () => {
axios.get.mockResolvedValue(useApiMockData)
const { result } = renderHook(() => useFetchData())
act(() => {
result.current.state = "SUCCESS"
result.current.error = ""
result.current.data = []
})
expect(result.current).toMatchObject({
data: [],
error: "",
state: "SUCCESS"
})
})
it("error state", async () => {
const errorMessage = "Network Error"
axios.get.mockImplementationOnce(() =>
Promise.reject(new Error(errorMessage))
)
const { result } = renderHook(() => useFetchData())
act(() => {
result.current.state = "ERROR"
result.current.error = "Fetch failed"
result.current.data = []
})
expect(result.current).toMatchObject({
data: [],
error: "Fetch failed",
state: "ERROR"
})
})
})
In the code block above, we are mocking our Axios call and the state updates. Notice how the act() method wraps code that causes any state updates as a result of an asynchronous request. Now, when we run our test, we see that everything passes as shown in the image below:

Testing React Hooks can be quite challenging. This section will cover some common pitfalls you may encounter and solutions for dealing with them.
There are certain scenarios in your React application when your React Hook may depend on certain operations, packages, or APIs. This can be difficult to isolate and test. If these dependencies are external APIs that can change at any time, simulating the behavior is even more challenging.
The solution to this challenge is to mock your dependencies. Mocking involves substituting real components or functions with simulated versions when running tests.
Below is an example of how to mock an API request:
import axios from 'axios';
jest.mock('axios');
const mockedData = [{
"id": 1,
"name": "Jimmy",
"isVerified": true,
}, {
"id": 2,
"name": "Tommy",
"isVerified": false,
}];
describe('Mocking an API', () => {
test('should fetch data from API, async () => {
axios.get.mockResolvedValue({ data: mockedData });
});
})
In the code block above, we imported the dependency our app will be using — in this case axios. Then, we mocked the dependency with Jest and faked the expected result
Side effects are any behaviors that occur outside the scope of the function being executed. An example of a side effect is data fetching or browser DOM manipulation. Testing side effects can be tricky because they are mostly asynchronous in nature, which can lead to unpredictable test outcomes.
The solution to this is to use the waitFor function from React Testing Library with async/await to wait for asynchronous operations to complete before making assertions:
import { waitFor } from '@testing-library/react';
test('should trigger side effect', async () => {
// Trigger the side effect
const resultPromise = someAsyncFunction();
// Wait for the promise to resolve
await waitFor(() => {
expect(resultPromise).resolves.toBe(/* expected value */);
});
});
The code block above shows how to use the waitFor function with async/await to wait for asynchronous code.
Testing custom Hooks in React can also be tricky because custom Hooks are different from components. You get an error when you try to test them using the render() function from React Testing Library, which stems from the fact that React Hooks cannot be used outside of React components.
The solution for this is to use the renderHook() function instead of the render() function. The renderHook() function provides an environment to render Hooks outside of components as shown in the code block below:
import { renderHook } from '@testing-library/react';
import useCustomHook from './useCustomHook';
test('should do something', () => {
const { result } = renderHook(() => useCustomHook());
// Assertions based on the hook's return value
expect(result.current.someValue).toBe(/* expected value */);
});
In the code above, we imported the renderHook() function and the custom Hook. Then, we successfully carried out our tests without errors.
Let’s compare Enzyme, Jest, and React Testing Library using metrics such as performance testing, mocking capabilities, scalability, and more:
| Metrics | Enzyme | Jest | React Testing Library |
|---|---|---|---|
| Performance | Performs well, but its extensive set of features can impact performance. Certain factors, like project size, may also limit performance | Highly performant | Known for its lightweight design, which contributes to better performance in rendering and querying |
| Usability | Provides rich features for component-centric testing and manipulation | General-purpose testing framework that offers a comprehensive set of features | Emphasizes simplicity, user-centric testing, and efficient querying of components |
| Snapshot testing | Supported via the enzyme-to-json package | Supported | Can handle snapshot testing but it wasn’t designed for that |
| Mocking capabilities | Powerful mocking capabilities and utilities | Comes with built-in mocking capabilities | Limited built-in mocking |
| Updates and maintenance | Actively maintained | Actively maintained | Actively maintained |
| Scalability | Becomes less scalable for large applications | Scales well for applications of any size | Scales well for applications of any size |
This table should give you a better idea of when and how to use each tool. Remember, as we discussed before, Jest is a great tool to use together with Enzyme or React Testing Library. For example, you could use Jest to handle assertions while using React Testing Library to manage the interaction with React components.
In this article, we reviewed how to use React Hooks and discussed how to write tests for React Hooks and React components using Jest, Enzyme, and React Testing Library. We also compared these three options for testing React components to better understand how each tool could fit into our testing strategy.
Additionally, we covered some of the challenges of testing React Hooks and solutions you can implement to deal with these challenges. You can check out the demo project we used to explore testing React Hooks on CodeSandbox.
Want to learn more? Check out this post on how to avoid common mistakes with React Hooks. If you have any questions or comments, you can share them below.

:has(), with examplesThe CSS :has() pseudo-class is a powerful new feature that lets you style parents, siblings, and more – writing cleaner, more dynamic CSS with less JavaScript.

Kombai AI converts Figma designs into clean, responsive frontend code. It helps developers build production-ready UIs faster while keeping design accuracy and code quality intact.

Discover what’s new in The Replay, LogRocket’s newsletter for dev and engineering leaders, in the October 22nd issue.

John Reilly discusses how software development has been changed by the innovations of AI: both the positives and the negatives.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up now
10 Replies to "How to test React Hooks"
Unfortunately, the tests at https://codesandbox.io/s/rqj0lymyn do not run due to
`Invariant Violation: Target container is not a DOM element.`
Its not about testing react hooks, but react component using hooks. :disappointed:
I was able to test a react component using hooks with enzyme.
good article, concise summary on the basics & tooling for react hook-component testing. thanks! how do you stub HTTP requests? for example, is there a way to intercept the fetch requests going into the first Formula 1 app on mount use effect hook?
Thank you for the aha moment!
It is just for testing simple react hooks function that you can get from anywhere. You haven’t added Testing for async hook component. :disappointed:
Good read! 👍
Nice Article! Can you please tell if there is any way to override the default value of the useState(0). For example if I initially want to render the counter using the value 1 instead of 0?
This article and the react addition of hooks makes me want to throw up. If you add state to a pure function, *that function is no longer pure*!! If you add effects to the function, *it is not a pure function!!!*
The reason this is hard to test is *because* the functions are no longer pure once you do this! There’s a huge amount of context that has to exist for that function to work, none of which is a passed in argument! You can’t have your cake and eat it too.
In the example with Testing async Hook functions I am getting the following error
Timed out in waitForNextUpdate after 1000ms.