Testing React Hooks with Jest and React Testing Library
This article provides a comprehensive guide on unit testing custom React Hooks using Jest, renderHook, act, and the React Testing Library, covering examples such as useCounter, useEventListener, useHover, and useMouse with detailed code snippets and best‑practice recommendations.
Testing React Hooks with Jest
The article explains how to write reliable unit tests for custom React Hooks, emphasizing that hooks must be tested within a component‑like environment and that Jest together with @testing-library/react provides the necessary tools.
Custom Hook Example: useCounter
A simple useCounter hook is defined with add , dec and set functions. The source code is shown and a basic test verifies that the counter increments correctly.
import { useState } from "react";
function useCounter(initialValue = 0) {
const [current, setCurrent] = useState(initialValue);
const add = (number = 1) => setCurrent(v => v + number);
const dec = (number = 1) => setCurrent(v => v - number);
const set = (number = 1) => setCurrent(number);
return [current, { add, dec, set }] as const;
}
export default useCounter;Testing with renderHook and act
Because React only allows hooks inside components, the article introduces renderHook (from @testing-library/react) and act to render and interact with hooks safely.
import { act, renderHook } from "@testing-library/react";
describe("useCounter test", () => {
it("increments number", async () => {
const { result } = renderHook(() => useCounter(7));
expect(result.current[0]).toEqual(7);
act(() => { result.current[1].add(); });
expect(result.current[0]).toEqual(8);
});
});useEventListener Hook
The article presents a reusable useEventListener hook that abstracts adding and removing event listeners, handling both DOM nodes and ref objects. It shows how to test click events by creating a div element, attaching the listener, and asserting the click count.
import { useEffect } from "react";
import { useLatest } from "../useLatest";
const useEventListener = (event, handler, target = window) => {
const handlerRef = useLatest(handler);
useEffect(() => {
let element = typeof target === "object" && "current" in target ? target.current : target;
if (!element?.addEventListener) return;
const listener = e => handlerRef.current(e);
element.addEventListener(event, listener);
return () => element.removeEventListener(event, listener);
}, [event, target]);
};
export default useEventListener;Tests use beforeEach / afterEach to mount and unmount a container, then verify that clicks on the container increment a counter while clicks on document.body do not.
Derived Hooks: useHover and useMouse
useHover builds on useEventListener to detect mouseenter/mouseleave and expose a boolean isHover . Tests render a button, call renderHook with the button element, and fire mouseEnter and mouseLeave events using fireEvent .
const useHover = (target, options) => {
const { onEnter, onLeave, onChange } = options || {};
const [isHover, setHover] = useState(false);
useEventListener('mouseenter', () => { onEnter?.(); onChange?.(true); setHover(true); }, target);
useEventListener('mouseleave', () => { onLeave?.(); onChange?.(false); setHover(false); }, target);
return isHover;
};
export default useHover;useMouse tracks mouse coordinates by listening to mousemove on the document (or a custom target) and returns an object with screen, client, and page positions. The article discusses why document.dispatchEvent does not update the state in a js‑dom environment and recommends using fireEvent.mouseMove instead.
export default (target = document) => {
const [state, setState] = useState(initState);
useEventListener('mousemove', (e) => {
const { screenX, screenY, clientX, clientY, pageX, pageY } = e;
setState({ screenX, screenY, clientX, clientY, pageX, pageY, elementX: NaN, elementY: NaN, elementH: NaN, elementW: NaN, elementPosX: NaN, elementPosY: NaN });
}, { target });
return state;
};Testing Utilities Overview
The article briefly reviews the render API (getBy..., queryBy..., findBy...), the most common query methods (getByText, getByRole, etc.), and the fireEvent API for simulating user interactions such as clicks, mouse moves, and keyboard events.
Environment and Tooling Tips
Set Jest's testEnvironment to jsdom for browser‑like tests.
Use @testing-library/react-hooks for React versions < 18, otherwise the built‑in renderHook from @testing-library/react works with React 18.
Run tests with --debug to see console output, or use VS Code extensions for better debugging.
Conclusion
By combining renderHook , act , render , and fireEvent , developers can thoroughly test custom hooks, achieve high coverage, and avoid common pitfalls such as missing DOM events in a simulated environment.
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.