Frontend Development 16 min read

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.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Testing React Hooks with Jest and React Testing Library

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.

frontendJavaScriptReacttestingHooksjest
Rare Earth Juejin Tech Community
Written by

Rare Earth Juejin Tech Community

Juejin, a tech community that helps developers grow.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.