0
# Hook Testing
1
2
Specialized utilities for testing React hooks in isolation without creating wrapper components manually. The `renderHook` utility provides a clean API for testing custom hooks with proper React lifecycle handling.
3
4
## Capabilities
5
6
### RenderHook Function
7
8
Renders a hook within a test component, providing access to the hook's return value and utilities for re-running with different props.
9
10
```typescript { .api }
11
/**
12
* Render a hook within a test component for isolated testing
13
* @param render - Function that calls the hook and returns its result
14
* @param options - Configuration options for hook rendering
15
* @returns RenderHookResult with hook result and utilities
16
*/
17
function renderHook<
18
Result,
19
Props,
20
Q extends Queries = typeof queries,
21
Container extends RendererableContainer | HydrateableContainer = HTMLElement,
22
BaseElement extends RendererableContainer | HydrateableContainer = Container,
23
>(
24
render: (initialProps: Props) => Result,
25
options?: RenderHookOptions<Props, Q, Container, BaseElement> | undefined,
26
): RenderHookResult<Result, Props>;
27
28
interface RenderHookOptions<
29
Props,
30
Q extends Queries = typeof queries,
31
Container extends RendererableContainer | HydrateableContainer = HTMLElement,
32
BaseElement extends RendererableContainer | HydrateableContainer = Container,
33
> extends RenderOptions<Q, Container, BaseElement> {
34
/** Initial props to pass to the hook function */
35
initialProps?: Props | undefined;
36
}
37
38
interface RenderHookResult<Result, Props> {
39
/** Current hook result with stable reference */
40
result: { current: Result };
41
/** Re-run hook with new props */
42
rerender: (props?: Props) => void;
43
/** Unmount test component */
44
unmount: () => void;
45
}
46
47
// Note: RenderHookOptions extends RenderOptions, so it includes:
48
// - container, baseElement, hydrate, legacyRoot, wrapper, queries, reactStrictMode
49
// - onCaughtError, onRecoverableError, onUncaughtError
50
// - All standard render configuration options
51
```
52
53
**Basic Usage:**
54
55
```typescript
56
import { renderHook } from "@testing-library/react";
57
import { useState } from "react";
58
59
function useCounter(initialValue = 0) {
60
const [count, setCount] = useState(initialValue);
61
const increment = () => setCount(c => c + 1);
62
const decrement = () => setCount(c => c - 1);
63
return { count, increment, decrement };
64
}
65
66
// Test the hook
67
const { result } = renderHook(() => useCounter(5));
68
69
expect(result.current.count).toBe(5);
70
71
// Test hook actions
72
act(() => {
73
result.current.increment();
74
});
75
76
expect(result.current.count).toBe(6);
77
```
78
79
### Hook Result Access
80
81
The `result` object provides stable access to the hook's current return value.
82
83
```typescript { .api }
84
/**
85
* Stable reference to hook result
86
*/
87
interface HookResult<Result> {
88
/** Current value returned by the hook */
89
current: Result;
90
}
91
```
92
93
**Usage Examples:**
94
95
```typescript
96
function useToggle(initial = false) {
97
const [value, setValue] = useState(initial);
98
const toggle = () => setValue(v => !v);
99
return [value, toggle] as const;
100
}
101
102
const { result } = renderHook(() => useToggle());
103
104
// Access return value
105
const [isToggled, toggle] = result.current;
106
expect(isToggled).toBe(false);
107
108
// Hook actions trigger re-renders
109
act(() => {
110
toggle();
111
});
112
113
// Result updates automatically
114
const [newIsToggled] = result.current;
115
expect(newIsToggled).toBe(true);
116
```
117
118
### Hook Re-rendering
119
120
Re-run hooks with different props to test various scenarios and prop changes.
121
122
```typescript { .api }
123
/**
124
* Re-run the hook with new props
125
* @param props - New props to pass to hook function
126
*/
127
rerender(props?: Props): void;
128
```
129
130
**Usage Examples:**
131
132
```typescript
133
function useLocalStorage(key: string, defaultValue: string) {
134
const [value, setValue] = useState(() => {
135
return localStorage.getItem(key) || defaultValue;
136
});
137
138
const updateValue = (newValue: string) => {
139
setValue(newValue);
140
localStorage.setItem(key, newValue);
141
};
142
143
return [value, updateValue] as const;
144
}
145
146
// Initial render
147
const { result, rerender } = renderHook(
148
({ key, defaultValue }) => useLocalStorage(key, defaultValue),
149
{
150
initialProps: { key: 'test-key', defaultValue: 'initial' }
151
}
152
);
153
154
expect(result.current[0]).toBe('initial');
155
156
// Test with different props
157
rerender({ key: 'different-key', defaultValue: 'different' });
158
expect(result.current[0]).toBe('different');
159
```
160
161
### Hook Unmounting
162
163
Clean up hook resources and test cleanup effects.
164
165
```typescript { .api }
166
/**
167
* Unmount the test component and cleanup hook
168
*/
169
unmount(): void;
170
```
171
172
**Usage Examples:**
173
174
```typescript
175
function useInterval(callback: () => void, delay: number) {
176
useEffect(() => {
177
const id = setInterval(callback, delay);
178
return () => clearInterval(id);
179
}, [callback, delay]);
180
}
181
182
const callback = jest.fn();
183
const { unmount } = renderHook(() => useInterval(callback, 1000));
184
185
// Let some time pass
186
jest.advanceTimersByTime(2000);
187
expect(callback).toHaveBeenCalledTimes(2);
188
189
// Unmount should clean up interval
190
unmount();
191
jest.advanceTimersByTime(1000);
192
expect(callback).toHaveBeenCalledTimes(2); // No additional calls
193
```
194
195
### Testing Hooks with Context
196
197
Use wrapper components to provide context to hooks that depend on providers.
198
199
**Usage Examples:**
200
201
```typescript
202
import { createContext, useContext } from "react";
203
204
const ThemeContext = createContext({ color: 'blue' });
205
206
function useTheme() {
207
return useContext(ThemeContext);
208
}
209
210
// Test hook with context
211
const { result } = renderHook(() => useTheme(), {
212
wrapper: ({ children }) => (
213
<ThemeContext.Provider value={{ color: 'red' }}>
214
{children}
215
</ThemeContext.Provider>
216
)
217
});
218
219
expect(result.current.color).toBe('red');
220
```
221
222
### Async Hook Testing
223
224
Test hooks that perform asynchronous operations using `waitFor`.
225
226
**Usage Examples:**
227
228
```typescript
229
import { waitFor } from "@testing-library/react";
230
231
function useAsyncData(url: string) {
232
const [data, setData] = useState(null);
233
const [loading, setLoading] = useState(true);
234
const [error, setError] = useState(null);
235
236
useEffect(() => {
237
let cancelled = false;
238
239
fetch(url)
240
.then(response => response.json())
241
.then(result => {
242
if (!cancelled) {
243
setData(result);
244
setLoading(false);
245
}
246
})
247
.catch(err => {
248
if (!cancelled) {
249
setError(err);
250
setLoading(false);
251
}
252
});
253
254
return () => { cancelled = true; };
255
}, [url]);
256
257
return { data, loading, error };
258
}
259
260
// Mock fetch
261
global.fetch = jest.fn(() =>
262
Promise.resolve({
263
json: () => Promise.resolve({ message: 'success' })
264
})
265
);
266
267
const { result } = renderHook(() => useAsyncData('/api/data'));
268
269
// Initially loading
270
expect(result.current.loading).toBe(true);
271
expect(result.current.data).toBe(null);
272
273
// Wait for async operation
274
await waitFor(() => {
275
expect(result.current.loading).toBe(false);
276
});
277
278
expect(result.current.data).toEqual({ message: 'success' });
279
```
280
281
### Hook Error Testing
282
283
Test error scenarios and error boundaries with hooks.
284
285
**Usage Examples:**
286
287
```typescript
288
function useThrowingHook(shouldThrow: boolean) {
289
if (shouldThrow) {
290
throw new Error('Hook error');
291
}
292
return 'success';
293
}
294
295
// Test error is thrown
296
expect(() => {
297
renderHook(() => useThrowingHook(true));
298
}).toThrow('Hook error');
299
300
// Test no error when condition is false
301
const { result } = renderHook(() => useThrowingHook(false));
302
expect(result.current).toBe('success');
303
```
304
305
### Complex Hook Testing
306
307
Test hooks with multiple state values and complex interactions.
308
309
**Usage Examples:**
310
311
```typescript
312
function useForm(initialValues: Record<string, any>) {
313
const [values, setValues] = useState(initialValues);
314
const [errors, setErrors] = useState({});
315
const [touched, setTouched] = useState({});
316
317
const setValue = (name: string, value: any) => {
318
setValues(prev => ({ ...prev, [name]: value }));
319
};
320
321
const setError = (name: string, error: string) => {
322
setErrors(prev => ({ ...prev, [name]: error }));
323
};
324
325
const setTouched = (name: string) => {
326
setTouched(prev => ({ ...prev, [name]: true }));
327
};
328
329
const reset = () => {
330
setValues(initialValues);
331
setErrors({});
332
setTouched({});
333
};
334
335
return {
336
values,
337
errors,
338
touched,
339
setValue,
340
setError,
341
setTouched,
342
reset
343
};
344
}
345
346
const { result } = renderHook(() => useForm({ name: '', email: '' }));
347
348
// Test initial state
349
expect(result.current.values).toEqual({ name: '', email: '' });
350
expect(result.current.errors).toEqual({});
351
352
// Test setValue
353
act(() => {
354
result.current.setValue('name', 'John');
355
});
356
357
expect(result.current.values.name).toBe('John');
358
359
// Test setError
360
act(() => {
361
result.current.setError('email', 'Invalid email');
362
});
363
364
expect(result.current.errors.email).toBe('Invalid email');
365
366
// Test reset
367
act(() => {
368
result.current.reset();
369
});
370
371
expect(result.current.values).toEqual({ name: '', email: '' });
372
expect(result.current.errors).toEqual({});
373
```