← Back to React Course | Chapter 12: Testing React Applications | Lesson 5 of 9

Testing Custom Hooks

Testing hooks is like testing a recipe by itself, separate from the whole restaurant, to make sure the recipe alone actually works.

Why You Can't Just Call a Hook in a Test

Hooks like useState rely on React's internal rendering machinery to track state between renders — calling useCounter() directly in a plain test function throws an error, since there's no component or render cycle for it to hook into.

Note: This is the same 'Rules of Hooks' restriction that applies everywhere else — hooks only work inside actual React rendering.

Warning: Attempting const result = useMyHook() directly at the top level of a test file always fails with an 'Invalid hook call' error.

Using renderHook to Test in Isolation

renderHook() from @testing-library/react wraps your hook in a minimal, invisible test component automatically, letting the hook run inside a real render cycle. The returned result object's .current property gives you whatever the hook returned.

Note: renderHook is specifically designed for testing custom hooks without needing to build a real component around them just for the test.

Warning: result.current reflects the hook's value at the time you read it — you need to re-read it after any update to see the new value.

Example: Using renderHook to Test in Isolation

markup
// Run in your local React project (npm install required)
import { renderHook } from '@testing-library/react';

function useCounter() {
  const [count, setCount] = React.useState(0);
  return { count, increment: () => setCount(c => c + 1) };
}

test('counter starts at 0', () => {
  const { result } = renderHook(() => useCounter());
  expect(result.current.count).toBe(0);
});

⚠️ This example uses an npm package with no CDN build available here — run this in your local React project.

Triggering Updates with act()

Calling a function returned by the hook (like increment()) needs to be wrapped in act(), which tells React to fully process the resulting state update and re-render before your test continues, ensuring result.current reflects the latest value.

Note: act() is often already handled automatically by newer testing-library versions for common cases, but wrapping explicitly is always safe.

Warning: Forgetting act() around a state-updating call can produce a warning and, in some cases, read result.current before the update has actually been applied.

Example: Triggering Updates with act()

markup
// Run in your local React project (npm install required)
import { renderHook, act } from '@testing-library/react';

function useCounter() {
  const [count, setCount] = React.useState(0);
  return { count, increment: () => setCount(c => c + 1) };
}

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

⚠️ This example uses an npm package with no CDN build available here — run this in your local React project.

Common Mistakes
  1. Trying to call a hook directly in a test function body, outside of any component — hooks can only run inside React's rendering system.
  2. Forgetting that state updates inside renderHook's act() calls need to be wrapped for React to process them correctly.
  3. Testing a hook completely in isolation when its real behavior only makes sense combined with a specific component context.
Chapter Summary
  • Hooks can't be called directly in a test — they need to run inside React's render cycle.
  • renderHook() from @testing-library/react wraps a hook in a minimal test component automatically.
  • The result object's .current property gives access to whatever the hook returns.
  • act() ensures state updates triggered during a test are fully processed before assertions run.
Browser Support

Requires npm install @testing-library/react — runs in a Node test environment, not this browser sandbox.

Login to run this code

C/C++/Java/PHP execution requires a free account. Your code is saved — you'll land right back in the editor after logging in.