JS Guide
HomeQuestionsTopicsCompaniesResources
BookmarksSearch

Built for developers preparing for JavaScript, React & TypeScript interviews.

ResourcesQuestionsSupport
HomeQuestionsSearchProgress
HomeTopicstestingTesting Custom React Hooks
PrevNext
testing
intermediate
12 min read

Testing Custom React Hooks

act
custom-hooks
react
renderHook
testing-library
waitFor

How to test custom React hooks using renderHook, handling state updates with act(), testing hook return values, and avoiding common pitfalls when hooks depend on context or async operations.

Key Points

1renderHook Utility

Renders a hook inside a minimal test component, exposing result.current for assertions and rerender for testing prop changes.

2act() for State Updates

State-changing operations in hooks must be wrapped in act() to ensure React processes updates synchronously before assertions are checked.

3Context and Async Patterns

Provide context via wrapper components in renderHook options, and use waitFor to assert on eventual state from async hooks.

4Direct vs Component Testing

Test hooks directly when they contain shared complex logic. Test through the component when integration behavior matters more than the hook in isolation.

What You'll Learn

  • Use renderHook to test custom hooks outside of components
  • Wrap state updates in act() to prevent stale assertions and warnings
  • Test hooks that depend on context providers and async operations
  • Decide when to test hooks directly versus through component integration

Deep Dive

Custom React hooks encapsulate reusable stateful logic, and testing them requires a different approach than testing components. Since hooks cannot be called outside of a React component, the testing-library provides renderHook to execute hooks in a lightweight test wrapper. Understanding how to test hooks is a common intermediate-level interview question.

Why Hooks Need Special Testing

React hooks must follow the Rules of Hooks -- they can only be called inside function components or other hooks. You cannot simply call useState() in a test file. The renderHook utility from @testing-library/react solves this by rendering your hook inside a minimal test component and exposing the hook's return value.

renderHook API

The renderHook function takes a callback that calls your hook and returns an object with result and rerender:

JavaScript
import { renderHook, act } from '@testing-library/react';
import { useCounter } from './useCounter';
 
test('initializes with default value', () => {
  const { result } = renderHook(() => useCounter(0));
  expect(result.current.count).toBe(0);
});

result.current always points to the latest return value of the hook. After state updates, result.current reflects the new state.

Handling State Updates with act()

When your hook triggers state updates (via useState, useReducer, etc.), those updates must be wrapped in act() to ensure React processes them synchronously before assertions:

JavaScript
test('increments the counter', () => {
  const { result } = renderHook(() => useCounter(0));
  act(() => {
    result.current.increment();
  });
  expect(result.current.count).toBe(1);
});

Forgetting act() causes "act" warnings and potentially stale result.current values. This is the most common pitfall when testing hooks.

Testing with Props/Arguments

To test how a hook responds to changing inputs, use the rerender function with new arguments:

JavaScript
test('updates when props change', () => {
  const { result, rerender } = renderHook(
    ({ initialCount }) => useCounter(initialCount),
    { initialProps: { initialCount: 0 } }
  );
  expect(result.current.count).toBe(0);
  rerender({ initialCount: 10 });
  expect(result.current.count).toBe(10);
});

Testing Hooks with Context

If your hook consumes a React context, provide the context via a wrapper component:

JavaScript
const wrapper = ({ children }) => (
  <ThemeProvider value="dark">{children}</ThemeProvider>
);
const { result } = renderHook(() => useTheme(), { wrapper });

Testing Async Hooks

Hooks that trigger async operations (data fetching, debounced updates) require waitFor to assert on eventual state:

JavaScript
test('fetches data on mount', async () => {
  const { result } = renderHook(() => useFetchUser(1));
  expect(result.current.loading).toBe(true);
  await waitFor(() => {
    expect(result.current.data).toEqual({ name: 'Alice' });
  });
});

When to Test Hooks Directly vs Through Components

Test hooks directly when they contain complex logic that multiple components share. If a hook is simple and only used in one component, testing through the component's behavior may be sufficient and more realistic. The general rule: test the hook directly when it is the unit you want to verify, test through the component when the integration matters more.

Fun Fact

renderHook was originally a separate package (@testing-library/react-hooks) maintained by a different team. It was merged into @testing-library/react in version 13.1.0, simplifying the ecosystem and eliminating a common source of version mismatch bugs.

Learn These First

React Testing Library

intermediate

Testing Asynchronous Code

beginner

Continue Learning

Mocking Functions, Modules, and APIs

intermediate

Integration Tests vs Unit Tests

intermediate

Practice What You Learned

How do you test custom React hooks?
mid
hooks
Use @testing-library/react's renderHook to test custom hooks in isolation. The function returns result.current with the hook's return value, and rerender to trigger updates. For hooks with state, use act() to wrap state updates.
Previous
End-to-End Testing
Next
Integration Tests vs Unit Tests
PrevNext