Testing Custom Hooks
In this page:
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
// 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()
// 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.
- Trying to call a hook directly in a test function body, outside of any component — hooks can only run inside React's rendering system.
- Forgetting that state updates inside renderHook's act() calls need to be wrapped for React to process them correctly.
- Testing a hook completely in isolation when its real behavior only makes sense combined with a specific component context.
- 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.
Requires npm install @testing-library/react — runs in a Node test environment, not this browser sandbox.
Chapter Quiz — Complete all 9 topics to unlock
0/9 topics done
Complete these topics first: