Testing Hooks

⭐ Interview Importance: MEDIUM
⏱️ Revision Time: 2 min

Testing Hooks

Custom Hooks encapsulate reusable stateful logic in React. Because they don’t render any UI themselves, testing them requires a slightly different approach than testing standard components.

The Problem with Testing Hooks

You cannot call a React Hook outside of a React component.

If you try to import a hook and call it directly in your test file, React will throw a fatal error: “Invalid hook call. Hooks can only be called inside of the body of a function component.”

// ❌ This will crash!
import { useCounter } from './useCounter';

it('increments the counter', () => {
  const { count, increment } = useCounter(); // ERROR! Not inside a component!
});

The Solution: @testing-library/react-hooks

(Note: In modern React Testing Library v13.1+, these functions have been merged directly into the core @testing-library/react package, so you no longer need the separate -hooks package).

To test a hook, RTL provides a renderHook function. Under the hood, RTL creates a tiny, invisible React component, calls your hook inside of it, and returns the results.

Example: Testing a Counter Hook

// useCounter.js
import { useState } from 'react';

export function useCounter(initialValue = 0) {
  const [count, setCount] = useState(initialValue);
  const increment = () => setCount((c) => c + 1);
  return { count, increment };
}
// useCounter.test.js
import { renderHook, act } from '@testing-library/react';
import { useCounter } from './useCounter';

describe('useCounter', () => {
  it('initializes with default value', () => {
    // 1. Render the hook
    const { result } = renderHook(() => useCounter());

    // 2. Access the returned values via result.current
    expect(result.current.count).toBe(0);
  });

  it('initializes with provided value', () => {
    // Pass arguments to the hook
    const { result } = renderHook(() => useCounter(10));
    expect(result.current.count).toBe(10);
  });

  it('increments the count', () => {
    const { result } = renderHook(() => useCounter(0));

    // 3. The 'act' function
    // Whenever you trigger a state update in a test (like calling increment),
    // you MUST wrap it in act(). This tells React to process the state update
    // and re-render the invisible test component before moving to the assertion.
    act(() => {
      result.current.increment();
    });

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

When NOT to use renderHook

renderHook is specifically designed for testing reusable, independent Custom Hooks (like useLocalStorage, useDebounce, or useAuth).

If a custom hook is completely coupled to one specific component and is never used anywhere else, you should not test the hook directly. Instead, you should write an integration test for the Component that uses the hook. If the Component works as expected from the user’s perspective, the hook is implicitly tested.

Interview Questions

Q: How do you test a custom hook that doesn’t render any UI?
A: You can use renderHook from @testing-library/react. It creates a dummy React component wrapper around your hook, allowing you to call the hook’s functions and assert its state changes safely within the rules of React.