Testing Library React Hooks: A Complete Guide

React Hooks changed the way we compose components, but testing them can be a mess—especially if you're using asynchronous data, having side effects that require cleanup logic, or need to check dependencies from React Context. The react-hooks-testing-library (deprecated now in favor of @testing-library/react) solves all of these problems, allowing developers to test hooks in isolation, configured for specific behavior.

In this tutorial we are going to see how to test custom React Hooks well using the Testing Library, explore Jest vs Vitest frameworks, and covering those parts omitted by other tutorials—such as error conditions, cleanup testing, and TypeScript support.

Why Test React Hooks?

Test React Hooks

Custom hooks encapsulate logic. If that logic is broken, your component tree can suffer. Unit testing hooks makes sure of the following:

  • Business logic is decoupled from your app and is trustworthy

  • Async actions like fetches or debouncing inputs are working as desired

  • If you have effects and cleanups they are triggered appropriately

By testing your hooks, you can validate your assumptions and separate the behavior you expect, and catch any bugs before they affect the user experience.

Want to learn more about unit testing? Read Automated Unit Testing: A Beginner’s Guide on our blog.

Migration Notice: From react-hooks-testing-library to @testing-library/react

If you're using React 18+, you should use the renderHook utility directly from @testing-library/react. The original react-hooks-testing-library is now archived and deprecated.

Install:

npm install --save-dev @testing-library/react

Import like so:

import { renderHook } from '@testing-library/react'

For older React versions, the react-hooks-testing-library package is still available but not recommended.

Getting Started: Core API

import { act, renderHook } from '@testing-library/react';

const useCounter = (initialCount = 0) => {
  const [count, setCount] = React.useState(initialCount);
  const increment = () => setCount(count => count + 1);
  return {
    count,
    increment,
  };
};

// test
const { result } = renderHook(() => useCounter());

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

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

Key API methods:

  • renderHook(fn) – run your hook function

  • result.current – contains returned values

  • act(cb) – wrap all state updates inside this

  • rerender(newProps) – useful for prop-driven hooks

  • unmount() – test cleanup logic

Async Hooks + API Mocking

Let’s say your hook fetches data on mount:

function useUser(userId) {
  const [user, setUser] = useState(null)
  useEffect(() => {
    fetch(`/api/user/${userId}`)
      .then((res) => res.json())
      .then(setUser)
  }, [userId])
  return user
}

Test it:

import { waitFor } from '@testing-library/react'
global.fetch = jest.fn(() => Promise.resolve({
  json: () => Promise.resolve({ name: 'Alice' })
}))

const { result } = renderHook(() => useUser(1))

await waitFor(() => expect(result.current).toEqual({ name: 'Alice' }))

Tips:

  • Use waitFor to wait for async state

  • Mock fetch or external libs (axios, etc.)

Want to dive deeper into API testing? Check out The Importance of API Testing for more insights.

Testing Errors in React 18

If your hook throws, Testing Library exposes result.error:

function useThrowingHook() {
  throw new Error('Oops')
}

const { result } = renderHook(() => useThrowingHook())
expect(result.error).toEqual(new Error('Oops'))

This is especially useful for guards, failed API calls, or invalid config inputs.

For structured test strategies, read How to Create Effective Test Cases.

Cleanup & Effects Testing

Hooks with side effects often use useEffect and need to clean up properly. To test this:

function useInterval(callback, delay) {
  useEffect(() => {
    const id = setInterval(callback, delay)
    return () => clearInterval(id)
  }, [callback, delay])
}

const clearIntervalSpy = jest.spyOn(global, 'clearInterval')
const { unmount } = renderHook(() => useInterval(() => {}, 1000))
unmount()
expect(clearIntervalSpy).toHaveBeenCalled()

Jest vs Vitest: Which to Use?

Jest:

  • Industry standard, widely supported

  • Slower with large projects

Vitest:

  • Vite-native, much faster

  • Native ESM support

  • Requires JSDOM setup for hooks

// vite.config.ts
export default defineConfig({
  test: {
    environment: 'jsdom',
  },
})

Choose based on project needs. Vitest is ideal for speed and modern setups.
Want a full comparison? Read our post on Switching from Jest to Vitest.

TypeScript Support

Testing Library supports TypeScript seamlessly:

function useTyped(input: number): string {
  return `Value: ${input}`
}

const { result } = renderHook(() => useTyped(5))
expect(result.current).toBe('Value: 5')

Typing result helps with better IDE support and test accuracy.


Best Practices & Common Pitfalls

Do’sDon’t
Focus on behavior instead of implementationDon't directly test internal state updates
Properly mock external APIsDon't skip cleanup testing
Use act for all state changesDon't combine concerns (test logic, not rendering)

Want to improve your test automation workflow? Read Test Automation: Everything You Need To Know.

Final Thoughts

Testing custom hooks is essential for reliable React applications. By using Testing Library with Jest or Vitest—and covering areas like async logic, cleanup, and TypeScript—you’ll gain confidence in your logic and ship fewer bugs.

For further reference, check out the official testing-library/react docs and stay up to date with the latest improvements.

Frequently Asked Questions

1. What is renderHook used for?

It allows you to run and test a custom React Hook in isolation without rendering a full component.

2. Should I use @testing-library/react or react-hooks-testing-library?

Use @testing-library/react for React 18+. The original library is deprecated.

3. How do I test hooks with async logic?

Use waitFor, jest.fn, or jest.spyOn to mock async behavior and wait for expected changes.

4. Can I use Vitest instead of Jest?

Yes. Vitest is a modern, faster alternative. Just ensure you're using jsdom for DOM support.

5. How do I test hook errors?

Check result.error after rendering the hook. Works with React 18.

0
Subscribe to my newsletter

Read articles from Himanshu Mandhyan directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Himanshu Mandhyan
Himanshu Mandhyan