Testing Custom Hooks

๐Ÿงช Introduction

Custom Hooks allow developers to extract reusable stateful logic from React components. Since they often encapsulate business logic, testing them independently ensures reliability, easier maintenance, and confidence during refactoring. This tutorial explains how to test custom hooks effectively using renderHook, assertions, and asynchronous testing utilities.

Important

Custom Hooks should be tested based on their behavior rather than their internal implementation.

๐ŸŽฏ Why Test Custom Hooks?

  • Verify state updates correctly.
  • Ensure side effects execute as expected.
  • Detect regressions during future changes.
  • Validate asynchronous operations.
  • Increase confidence when reusing hooks across multiple components.

๐Ÿงฉ Understanding the Testing Workflow

Create Custom Hook
Render the Hook
Interact with Hook
Assert Expected Results
Trigger State Updates
Call Returned Functions

๐Ÿ“ฆ Required Testing Utilities

UtilityPurpose
renderHookRenders a custom hook for testing.
actWraps state updates to simulate user interactions.
waitForWaits for asynchronous updates.
expectPerforms assertions.

๐Ÿ“ Example Project Structure

src
hooks
useCounter.js
useFetch.js
tests
useCounter.test.js
useFetch.test.js

โš™๏ธ Example Custom Hook

useCounter.js

import { useState } from "react";

export function useCounter(initialValue = 0) {
  const [count, setCount] = useState(initialValue);

  const increment = () => setCount(c => c + 1);
  const decrement = () => setCount(c => c - 1);
  const reset = () => setCount(initialValue);

  return {
    count,
    increment,
    decrement,
    reset
  };
}

๐Ÿงช Testing the Hook

useCounter.test.js

import { renderHook, act } from "@testing-library/react";
import { useCounter } from "../hooks/useCounter";

test("increments counter", () => {
  const { result } = renderHook(() => useCounter());

  act(() => {
    result.current.increment();
  });

  expect(result.current.count).toBe(1);
});

The renderHook function mounts the hook in an isolated environment, while act ensures React processes all state updates before assertions are made.

๐Ÿ”„ Testing Multiple Actions

Multiple Actions

test("increment, decrement and reset", () => {
  const { result } = renderHook(() => useCounter(5));

  act(() => {
    result.current.increment();
    result.current.increment();
    result.current.decrement();
  });

  expect(result.current.count).toBe(6);

  act(() => {
    result.current.reset();
  });

  expect(result.current.count).toBe(5);
});

โณ Testing Asynchronous Hooks

Hooks performing API calls or delayed updates require waiting for asynchronous state changes using waitFor.

Async Hook Test

import { renderHook, waitFor } from "@testing-library/react";

test("loads data", async () => {
  const { result } = renderHook(() => useFetch());

  await waitFor(() => {
    expect(result.current.loading).toBe(false);
  });

  expect(result.current.data).toBeDefined();
});

๐Ÿ“… Typical Testing Lifecycle

๐Ÿ“– Common Test Scenarios

Verify that initial values, updates, and resets behave correctly after each interaction.

Ensure every function returned by the hook performs the intended logic and produces the expected state.

Use waitFor to validate delayed updates, network responses, or timers before making assertions.

โš ๏ธ Best Practices

  • Test behavior instead of implementation details.
  • Wrap all state-changing operations with act.
  • Write independent tests that do not rely on previous executions.
  • Cover both successful and edge-case scenarios.
  • Mock external services when testing API-dependent hooks.
  • Keep tests small, readable, and focused.

Best Practice

Prefer testing the public API returned by a custom hook rather than inspecting internal state management details.

๐Ÿ“š Additional Resources

Refer to the official React documentation and the Testing Library documentation for more examples and advanced testing techniques.

๐Ÿ“ Summary

Testing Custom Hooks involves rendering the hook, interacting with its exposed API, and verifying outcomes through assertions. Utilities such as renderHook, act, and waitFor simplify testing synchronous and asynchronous behavior while encouraging maintainable, production-ready React applications.

Summary

Test what the hook exposes, verify observable behavior, handle asynchronous updates properly, and keep tests isolated for maximum reliability.