๐งช Introduction
Custom Hooks allow developers to extract reusable stateful logic from React components. Since they often encapsulate business logic, testing them independently ensures reliability, easier maintenance, and confidence during refactoring. This tutorial explains how to test custom hooks effectively using renderHook, assertions, and asynchronous testing utilities.
Important
๐ฏ Why Test Custom Hooks?
- Verify state updates correctly.
- Ensure side effects execute as expected.
- Detect regressions during future changes.
- Validate asynchronous operations.
- Increase confidence when reusing hooks across multiple components.
๐งฉ Understanding the Testing Workflow
๐ฆ Required Testing Utilities
| Utility | Purpose |
|---|---|
| renderHook | Renders a custom hook for testing. |
| act | Wraps state updates to simulate user interactions. |
| waitFor | Waits for asynchronous updates. |
| expect | Performs assertions. |
๐ Example Project Structure
โ๏ธ Example Custom Hook
useCounter.js
import { useState } from "react";
export function useCounter(initialValue = 0) {
const [count, setCount] = useState(initialValue);
const increment = () => setCount(c => c + 1);
const decrement = () => setCount(c => c - 1);
const reset = () => setCount(initialValue);
return {
count,
increment,
decrement,
reset
};
}๐งช Testing the Hook
useCounter.test.js
import { renderHook, act } from "@testing-library/react";
import { useCounter } from "../hooks/useCounter";
test("increments counter", () => {
const { result } = renderHook(() => useCounter());
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});The renderHook function mounts the hook in an isolated environment, while act ensures React processes all state updates before assertions are made.
๐ Testing Multiple Actions
Multiple Actions
test("increment, decrement and reset", () => {
const { result } = renderHook(() => useCounter(5));
act(() => {
result.current.increment();
result.current.increment();
result.current.decrement();
});
expect(result.current.count).toBe(6);
act(() => {
result.current.reset();
});
expect(result.current.count).toBe(5);
});โณ Testing Asynchronous Hooks
Hooks performing API calls or delayed updates require waiting for asynchronous state changes using waitFor.
Async Hook Test
import { renderHook, waitFor } from "@testing-library/react";
test("loads data", async () => {
const { result } = renderHook(() => useFetch());
await waitFor(() => {
expect(result.current.loading).toBe(false);
});
expect(result.current.data).toBeDefined();
});๐ Typical Testing Lifecycle
Create or identify the custom hook to test.
Render the hook using renderHook.
Trigger actions or state updates inside act.
Verify the expected output using assertions.
Repeat for edge cases and asynchronous scenarios.
๐ Common Test Scenarios
Verify that initial values, updates, and resets behave correctly after each interaction.
Ensure every function returned by the hook performs the intended logic and produces the expected state.
Use waitFor to validate delayed updates, network responses, or timers before making assertions.
โ ๏ธ Best Practices
- Test behavior instead of implementation details.
- Wrap all state-changing operations with act.
- Write independent tests that do not rely on previous executions.
- Cover both successful and edge-case scenarios.
- Mock external services when testing API-dependent hooks.
- Keep tests small, readable, and focused.
Best Practice
๐ Additional Resources
Refer to the official React documentation and the Testing Library documentation for more examples and advanced testing techniques.
๐ Summary
Testing Custom Hooks involves rendering the hook, interacting with its exposed API, and verifying outcomes through assertions. Utilities such as renderHook, act, and waitFor simplify testing synchronous and asynchronous behavior while encouraging maintainable, production-ready React applications.