Testing Hooks
Custom hooks are reusable behavior.
You can often test them through a component that uses the hook.
When the hook has complex logic and no meaningful UI, hook-level tests can be useful.
Prefer Testing Through Components First
function CounterButton() {
const { count, increment } = useCounter();
return <button onClick={increment}>Count: {count}</button>;
}
test("increments the counter", async () => {
const user = userEvent.setup();
render(<CounterButton />);
await user.click(screen.getByRole("button", { name: /count: 0/i }));
expect(screen.getByRole("button", { name: /count: 1/i })).toBeInTheDocument();
});This tests the hook through observable behavior.
Testing With renderHook
Testing Library provides renderHook in modern versions.
import { renderHook, act } from "@testing-library/react";
test("toggles a boolean", () => {
const { result } = renderHook(() => useToggle(false));
act(() => {
result.current.toggle();
});
expect(result.current.value).toBe(true);
});Use act when an update happens outside normal user-event helpers.
Hooks With Props
test("uses the initial value", () => {
const { result, rerender } = renderHook(
({ initialValue }) => useCounter(initialValue),
{ initialProps: { initialValue: 5 } }
);
expect(result.current.count).toBe(5);
rerender({ initialValue: 10 });
expect(result.current.count).toBe(5);
});This example shows an important hook edge case: initial state does not automatically reset when props change.
Hooks With Providers
If a hook reads context, wrap it.
const wrapper = ({ children }: { children: React.ReactNode }) => (
<ThemeProvider initialMode="dark">{children}</ThemeProvider>
);
const { result } = renderHook(() => useTheme(), { wrapper });The wrapper should match the real provider contract.
Async Hooks
For async hooks, wait for observable state.
test("loads data", async () => {
const { result } = renderHook(() => useUser("u1"));
await waitFor(() => {
expect(result.current.status).toBe("success");
});
expect(result.current.user?.name).toBe("Ava");
});Avoid arbitrary timeouts.
Wait for the condition that matters.
Common Mistakes
- Testing every hook directly when a component test would provide better confidence.
- Forgetting
actfor direct state updates. - Assuming changed props reset internal state.
- Not wrapping hooks that require context.
- Using real network requests in hook tests.
When is renderHook most useful?
Practical Challenge
Test a useDebouncedValue(value, delay) hook.
Cover:
- initial value
- value does not change before the delay
- value updates after the delay
- cleanup when the value changes quickly
Use fake timers if your test runner supports them.
Recap
Test hooks through components when that gives clear user-facing confidence.
Use renderHook for focused hook logic, wrap required providers, and wait for real conditions instead of sleeping in tests.