Building Your Learning Module...
Getting things ready for you!
Find videos you like?
Save to resource drawer for future reference!
Custom hooks contain reusable logic that you want to test independently from components. Testing hooks in isolation ensures your logic works correctly before using it across your app.
React Testing Library provides renderHook() specifically for testing hooks without needing a full component!
Test hook logic independently without rendering components.
No DOM rendering needed, hooks tests run lightning fast.
Ensure your shared hook logic works before using it everywhere.
See a custom hook in action! This useCounter hook provides state and functions. We'll learn to test it in isolation.
<div id="root"></div>Loading preview...
The renderHook() function from React Testing Library lets you test hooks without creating a component. It returns the hook's result and helper functions.
1. Call renderHook
const { result } = renderHook(() => useCounter())2. Access Current Value
result.current.count // Get hook's return value3. Call Hook Functions
act(() => result.current.increment())4. Assert New Value
expect(result.current.count).toBe(1)result.current. This ensures you're reading the most up-to-date value after state changes!Test hook initialization and state updates
import { renderHook, act } from '@testing-library/react';
import { useCounter } from './useCounter';
describe('useCounter Hook', () => {
test('initializes with default value of 0', () => {
const { result } = renderHook(() => useCounter());
expect(result.current.count).toBe(0);
});
test('initializes with custom value', () => {
const { result } = renderHook(() => useCounter(10));
expect(result.current.count).toBe(10);
});
test('increments count', () => {
const { result } = renderHook(() => useCounter());
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
test('decrements count', () => {
const { result } = renderHook(() => useCounter(5));
act(() => {
result.current.decrement();
});
expect(result.current.count).toBe(4);
});
test('resets to initial value', () => {
const { result } = renderHook(() => useCounter(10));
act(() => {
result.current.increment();
result.current.increment();
});
expect(result.current.count).toBe(12);
act(() => {
result.current.reset();
});
expect(result.current.count).toBe(10);
});
});Use rerender to update hook arguments
import { renderHook } from '@testing-library/react';
import { useUserData } from './useUserData';
describe('useUserData Hook - Props', () => {
test('returns data for initial user ID', () => {
const { result } = renderHook(
({ userId }) => useUserData(userId),
{ initialProps: { userId: 1 } }
);
expect(result.current.userId).toBe(1);
});
test('updates when user ID changes', () => {
const { result, rerender } = renderHook(
({ userId }) => useUserData(userId),
{ initialProps: { userId: 1 } }
);
expect(result.current.userId).toBe(1);
// Rerender with new props
rerender({ userId: 2 });
expect(result.current.userId).toBe(2);
});
test('handles multiple prop updates', () => {
const { result, rerender } = renderHook(
({ userId }) => useUserData(userId),
{ initialProps: { userId: 1 } }
);
expect(result.current.userId).toBe(1);
rerender({ userId: 2 });
expect(result.current.userId).toBe(2);
rerender({ userId: 3 });
expect(result.current.userId).toBe(3);
});
});Test hooks that use useEffect or call APIs
import { renderHook, waitFor } from '@testing-library/react';
import { useDocumentTitle } from './useDocumentTitle';
describe('useDocumentTitle Hook - Side Effects', () => {
// Store original title
const originalTitle = document.title;
afterEach(() => {
// Restore original title after each test
document.title = originalTitle;
});
test('sets document title on mount', () => {
renderHook(() => useDocumentTitle('New Title'));
expect(document.title).toBe('New Title');
});
test('updates document title when it changes', () => {
const { rerender } = renderHook(
({ title }) => useDocumentTitle(title),
{ initialProps: { title: 'First Title' } }
);
expect(document.title).toBe('First Title');
rerender({ title: 'Second Title' });
expect(document.title).toBe('Second Title');
});
test('restores original title on unmount', () => {
const { unmount } = renderHook(() => useDocumentTitle('Test Title'));
expect(document.title).toBe('Test Title');
unmount();
expect(document.title).toBe(originalTitle);
});
});Handle async operations with waitFor
import { renderHook, waitFor } from '@testing-library/react';
import { useFetch } from './useFetch';
// Mock fetch API
global.fetch = jest.fn();
describe('useFetch Hook - Async', () => {
beforeEach(() => {
fetch.mockClear();
});
test('starts with loading state', () => {
fetch.mockResolvedValue({
json: async () => ({ data: 'test' })
});
const { result } = renderHook(() => useFetch('/api/data'));
expect(result.current.loading).toBe(true);
expect(result.current.data).toBe(null);
});
test('fetches and returns data', async () => {
const mockData = { id: 1, name: 'Test' };
fetch.mockResolvedValue({
json: async () => mockData
});
const { result } = renderHook(() => useFetch('/api/users/1'));
expect(result.current.loading).toBe(true);
// Wait for async operation to complete
await waitFor(() => {
expect(result.current.loading).toBe(false);
});
expect(result.current.data).toEqual(mockData);
expect(result.current.error).toBe(null);
});
test('handles fetch errors', async () => {
const errorMessage = 'Network error';
fetch.mockRejectedValue(new Error(errorMessage));
const { result } = renderHook(() => useFetch('/api/data'));
await waitFor(() => {
expect(result.current.loading).toBe(false);
});
expect(result.current.error).toBe(errorMessage);
expect(result.current.data).toBe(null);
});
});Wrap state-changing functions in act()
Use rerender() to update hook props
Wait for async updates with waitFor()
Test cleanup with unmount()
Use renderHook() to test hooks without components. This makes tests faster and more focused on the hook's logic.
Access hook values through result.current. This ensures you read the latest state after updates. Don't destructure directly!
Wrap all state-changing function calls in act(). This ensures React processes all updates before assertions.
If your hook has side effects, test that cleanup happens properly using unmount().
Test hooks in isolation without creating components. Returns result object.
Always access hook values through result.current for latest state.
Wrap state-changing calls in act() to ensure all updates are processed.
Update hook props/arguments by calling rerender with new values.
Handle async operations by waiting for conditions to be met.
Use unmount() to test side effect cleanup and memory leaks.
You now know how to test custom hooks in isolation! Next topics: