or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

async-testing.mdasync.mdcomponent-rendering.mdconfiguration.mdelement-queries.mdevent-simulation.mdevents.mdhook-testing.mdhooks.mdindex.mdqueries.mdquick-reference.mdrendering.md

hook-testing.mddocs/

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

```