🚀 LEVEL UP TO SENIOR:Unlock 500+ Advanced Practical Challenges & Exercises.
🎓 COURSERA PARTNER:Earn professional Google, Meta, and IBM certificates to supercharge your resume.
REFERENCEreact

react Documentation

LOADING ENGINE...

How to Perform Hooks Testing in React

AI & DATA SCIENCE // hooks-testing

Testing custom hooks means verifying their returned values and behavior in response to changing inputs, typically using React Testing Library's renderHook utility rather than testing them through an actual rendered component.

Syntax

import { renderHook, act } from '@testing-library/react';
const { result } = renderHook(() => useMyHook());

Deep Dive Course

renderHook() renders a custom hook inside a minimal, invisible test component internally, giving direct access to whatever the hook returns via result.current, without needing to build and render a full, separate real component just to exercise that hook's logic. Since calling a hook's returned functions, like a setter or a custom action, typically causes React state updates, and therefore a re-render, those specific calls need to be wrapped in the act() utility, which ensures all resulting updates are fully applied and flushed before the test's subsequent assertions run against the hook's updated result.current value.

1Understanding How to Perform Hooks Testing in React

renderHook() renders a custom hook inside a minimal, invisible test component internally, giving direct access to whatever the hook returns via result.current, without needing to build and render a full, separate real component just to exercise that hook's logic. Since calling a hook's returned functions, like a setter or a custom action, typically causes React state updates, and therefore a re-render, those specific calls need to be wrapped in the act() utility, which ensures all resulting updates are fully applied and flushed before the test's subsequent assertions run against the hook's updated result.current value.

💡

Wrap any call to a function returned by a tested hook, like a state setter, in act() — forgetting to do so produces a React warning about state updates not being wrapped in act(), and can lead to assertions checking a stale value before the update has actually been applied.

editor.html
import { renderHook, act } from '@testing-library/react';
import { useCounter } from './useCounter';

test('useCounter starts at 0 and increments', () => {
  const { result } = renderHook(() => useCounter());
  expect(result.current.count).toBe(0);
  act(() => {
    result.current.increment();
  });
  expect(result.current.count).toBe(1);
});
localhost:3000

2Practical Example

Here is a real-world application of How to Perform Hooks Testing in React showing how it is used in production React code.

editor.html
import { renderHook } from '@testing-library/react';
import { useCounter } from './useCounter';

test('useCounter accepts a custom initial value', () => {
  const { result } = renderHook(() => useCounter(10));
  expect(result.current.count).toBe(10);
});
localhost:3000

3Best Practices

Follow these guidelines when working with How to Perform Hooks Testing in React:

1. Use renderHook() to test a custom hook's logic in isolation, rather than needing to build a full component solely to exercise that hook

2. Wrap calls to any function returned by the hook, like a setter or an action, in act(), ensuring the resulting update is fully applied before asserting against result.current

3. Focus hook tests on the hook's actual returned values and behavior across different inputs, rather than any specific component that might eventually consume it

⚠️

Tip: Wrap any call to a function returned by a tested hook, like a state setter, in act() — forgetting to do so produces a React warning about state updates not being wrapped in act(), and can lead to assertions checking a stale value before the update has actually been applied.

editor.html
import { renderHook, act } from '@testing-library/react';
import { useCounter } from './useCounter';

test('useCounter starts at 0 and increments', () => {
  const { result } = renderHook(() => useCounter());
  expect(result.current.count).toBe(0);
  act(() => {
    result.current.increment();
  });
  expect(result.current.count).toBe(1);
});
localhost:3000

Examples

Example 01Basic Usage
import { renderHook, act } from '@testing-library/react';
import { useCounter } from './useCounter';

test('useCounter starts at 0 and increments', () => {
  const { result } = renderHook(() => useCounter());
  expect(result.current.count).toBe(0);
  act(() => {
    result.current.increment();
  });
  expect(result.current.count).toBe(1);
});
Example 02Advanced Example
import { renderHook } from '@testing-library/react';
import { useCounter } from './useCounter';

test('useCounter accepts a custom initial value', () => {
  const { result } = renderHook(() => useCounter(10));
  expect(result.current.count).toBe(10);
});

Best Practices

  • Use renderHook() to test a custom hook's logic in isolation, rather than needing to build a full component solely to exercise that hook
  • Wrap calls to any function returned by the hook, like a setter or an action, in act(), ensuring the resulting update is fully applied before asserting against result.current
  • Focus hook tests on the hook's actual returned values and behavior across different inputs, rather than any specific component that might eventually consume it

Interview Question

Why does calling a function returned by a tested hook, like an increment function from useCounter, need to be wrapped in act() inside a test?

Hint: Think about what that function call actually does internally — does it just return a value, or does it also trigger a React state update and re-render?

A function like increment() returned by a stateful custom hook typically calls a state setter internally, which schedules a React state update and a subsequent re-render of the internal test component that renderHook() uses to host the hook — outside of React's own normal event-handling and rendering machinery, React doesn't automatically know when it's an appropriate, safe moment to apply and fully flush that update and its resulting re-render. act() explicitly tells React that everything inside this block that might cause an update should be fully processed and settled before moving on, ensuring that by the time the code immediately after the act() block runs, any resulting state update and re-render has already been applied, so result.current reflects the accurate, fully-updated value rather than a stale one from just before the update actually took effect. Skipping act() risks the test's subsequent assertion reading result.current before React has actually finished processing and applying that update, leading to flaky, timing-dependent test failures.

Exercises

MediumPractice using How to Perform Hooks Testing in React in a real scenario.
View Solution
import { renderHook, act } from '@testing-library/react';
import { useCounter } from './useCounter';

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

Frequently Asked Questions

Why does calling a function returned by a tested hook, like an increment function from useCounter, need to be wrapped in act() inside a test?

A function like increment() returned by a stateful custom hook typically calls a state setter internally, which schedules a React state update and a subsequent re-render of the internal test component that renderHook() uses to host the hook — outside of React's own normal event-handling and rendering machinery, React doesn't automatically know when it's an appropriate, safe moment to apply and fully flush that update and its resulting re-render. act() explicitly tells React that everything inside this block that might cause an update should be fully processed and settled before moving on, ensuring that by the time the code immediately after the act() block runs, any resulting state update and re-render has already been applied, so result.current reflects the accurate, fully-updated value rather than a stale one from just before the update actually took effect. Skipping act() risks the test's subsequent assertion reading result.current before React has actually finished processing and applying that update, leading to flaky, timing-dependent test failures.

Related Functions

react-testing-libraryusestatecomponent-testing