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?
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 functionresult.current
– contains returned valuesact(cb)
– wrap all state updates inside thisrerender(newProps)
– useful for prop-driven hooksunmount()
– 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 stateMock
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’s | Don’t |
Focus on behavior instead of implementation | Don't directly test internal state updates |
Properly mock external APIs | Don't skip cleanup testing |
Use act for all state changes | Don'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.
Subscribe to my newsletter
Read articles from Himanshu Mandhyan directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
