How to Test Custom Hooks in React
Testing custom hooks in React is essential for ensuring your
application’s business logic remains robust, predictable, and bug-free.
This article provides a straightforward guide on how to test React
custom hooks using modern tools like React Testing Library and Jest,
covering the use of renderHook for execution and
act for handling state updates.
The Standard Tool: React Testing Library
Because React hooks cannot be called outside of a React component,
you cannot test them like standard JavaScript functions. React Testing
Library provides a built-in utility called renderHook
specifically designed to solve this problem by wrapping the hook in a
temporary, functional component during the test execution.
Step 1: Create a Simple Custom Hook
To demonstrate testing, let us look at a simple custom hook called
useCounter that manages a count state and provides
increment and decrement functions:
import { useState, useCallback } from 'react';
export function useCounter(initialValue = 0) {
const [count, setCount] = useState(initialValue);
const increment = useCallback(() => setCount((x) => x + 1), []);
const decrement = useCallback(() => setCount((x) => x - 1), []);
return { count, increment, decrement };
}Step 2: Write the Test with renderHook
To test this hook, import renderHook from
@testing-library/react. The renderHook
function returns an object containing a result property.
The result.current property always points to the latest
return value of the hook.
import { renderHook, act } from '@testing-library/react';
import { useCounter } from './useCounter';
test('should initialize and update counter', () => {
// Render the hook
const { result } = renderHook(() => useCounter(10));
// Assert initial state
expect(result.current.count).toBe(10);
});Step 3: Testing State Updates with act()
When testing code that triggers state updates (like calling
increment or decrement), you must wrap those
interactions inside the act() utility. This ensures all
state updates and effects are processed and flushed to the virtual DOM
before you make your assertions.
test('should increment and decrement counter', () => {
const { result } = renderHook(() => useCounter(0));
// Trigger state update
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
// Trigger another state update
act(() => {
result.current.decrement();
});
expect(result.current.count).toBe(0);
});Testing Hooks with Dependencies
If your custom hook relies on context providers (such as Redux, React
Router, or Theme Providers), you can pass a wrapper option
to renderHook to supply the necessary context.
import { ThemeProvider } from './ThemeContext';
test('should use theme context value', () => {
const wrapper = ({ children }) => (
<ThemeProvider value="dark">{children}</ThemeProvider>
);
const { result } = renderHook(() => useThemeHook(), { wrapper });
expect(result.current.theme).toBe('dark');
});