0
# Act Utilities
1
2
State update wrapping utilities that ensure proper timing and batching of React updates during testing. The `act` utility is essential for testing hooks that perform state updates or side effects.
3
4
## Capabilities
5
6
### act Function
7
8
Wraps code that causes React state updates, ensuring that all updates are flushed and effects are executed before the act call completes. This is crucial for predictable testing of hooks.
9
10
```typescript { .api }
11
/**
12
* Wraps synchronous code that causes state updates
13
* @param callback - Synchronous function that triggers state updates
14
*/
15
function act(callback: () => void | undefined): void;
16
17
/**
18
* Wraps asynchronous code that causes state updates
19
* @param callback - Asynchronous function that triggers state updates
20
* @returns Promise that resolves when all updates are complete
21
*/
22
function act(callback: () => Promise<void | undefined>): Promise<undefined>;
23
```
24
25
**Usage Examples:**
26
27
```typescript
28
import { renderHook, act } from "@testing-library/react-hooks";
29
import { useState } from "react";
30
31
function useCounter(initialCount = 0) {
32
const [count, setCount] = useState(initialCount);
33
const increment = () => setCount(prev => prev + 1);
34
const decrement = () => setCount(prev => prev - 1);
35
return { count, increment, decrement };
36
}
37
38
test("synchronous state updates with act", () => {
39
const { result } = renderHook(() => useCounter(0));
40
41
expect(result.current.count).toBe(0);
42
43
// Wrap synchronous state updates in act
44
act(() => {
45
result.current.increment();
46
});
47
48
expect(result.current.count).toBe(1);
49
50
act(() => {
51
result.current.increment();
52
result.current.increment();
53
});
54
55
expect(result.current.count).toBe(3);
56
});
57
58
// Asynchronous example
59
function useAsyncCounter() {
60
const [count, setCount] = useState(0);
61
62
const incrementAsync = async () => {
63
await new Promise(resolve => setTimeout(resolve, 100));
64
setCount(prev => prev + 1);
65
};
66
67
return { count, incrementAsync };
68
}
69
70
test("asynchronous state updates with act", async () => {
71
const { result } = renderHook(() => useAsyncCounter());
72
73
expect(result.current.count).toBe(0);
74
75
// Wrap asynchronous state updates in act
76
await act(async () => {
77
await result.current.incrementAsync();
78
});
79
80
expect(result.current.count).toBe(1);
81
});
82
```
83
84
### When to Use act
85
86
Use `act` whenever you're triggering state updates or side effects in your hooks during tests:
87
88
**State Updates:**
89
```typescript
90
// ✅ Correct - wrap state updates
91
act(() => {
92
result.current.setValue("new value");
93
});
94
95
// ❌ Incorrect - unwrapped state update
96
result.current.setValue("new value");
97
```
98
99
**Effect Triggers:**
100
```typescript
101
function useDebounce(value: string, delay: number) {
102
const [debouncedValue, setDebouncedValue] = useState(value);
103
104
useEffect(() => {
105
const timer = setTimeout(() => setDebouncedValue(value), delay);
106
return () => clearTimeout(timer);
107
}, [value, delay]);
108
109
return debouncedValue;
110
}
111
112
test("debounce hook", async () => {
113
jest.useFakeTimers();
114
const { result, rerender } = renderHook(
115
({ value, delay }) => useDebounce(value, delay),
116
{ initialProps: { value: "initial", delay: 500 } }
117
);
118
119
expect(result.current).toBe("initial");
120
121
// Update props that trigger useEffect
122
act(() => {
123
rerender({ value: "updated", delay: 500 });
124
});
125
126
// Fast-forward timers
127
act(() => {
128
jest.advanceTimersByTime(500);
129
});
130
131
expect(result.current).toBe("updated");
132
});
133
```
134
135
**Event Handlers:**
136
```typescript
137
function useClickCounter() {
138
const [count, setCount] = useState(0);
139
const [ref, setRef] = useState<HTMLElement | null>(null);
140
141
useEffect(() => {
142
if (!ref) return;
143
144
const handleClick = () => setCount(prev => prev + 1);
145
ref.addEventListener("click", handleClick);
146
147
return () => ref.removeEventListener("click", handleClick);
148
}, [ref]);
149
150
return { count, ref: setRef };
151
}
152
153
test("click counter hook", () => {
154
const { result } = renderHook(() => useClickCounter());
155
156
const element = document.createElement("button");
157
158
act(() => {
159
result.current.ref(element);
160
});
161
162
expect(result.current.count).toBe(0);
163
164
act(() => {
165
element.click();
166
});
167
168
expect(result.current.count).toBe(1);
169
});
170
```
171
172
### act with Multiple Updates
173
174
`act` can wrap multiple state updates that should be batched together:
175
176
```typescript
177
function useMultiState() {
178
const [name, setName] = useState("");
179
const [age, setAge] = useState(0);
180
const [email, setEmail] = useState("");
181
182
const updateAll = (newName: string, newAge: number, newEmail: string) => {
183
setName(newName);
184
setAge(newAge);
185
setEmail(newEmail);
186
};
187
188
return { name, age, email, updateAll };
189
}
190
191
test("multiple state updates", () => {
192
const { result } = renderHook(() => useMultiState());
193
194
act(() => {
195
result.current.updateAll("Alice", 25, "alice@example.com");
196
});
197
198
expect(result.current.name).toBe("Alice");
199
expect(result.current.age).toBe(25);
200
expect(result.current.email).toBe("alice@example.com");
201
});
202
```
203
204
### Error Handling in act
205
206
If an error occurs within `act`, it will be propagated:
207
208
```typescript
209
function useErrorHook() {
210
const [shouldError, setShouldError] = useState(false);
211
212
const triggerError = () => setShouldError(true);
213
214
if (shouldError) {
215
throw new Error("Hook error occurred");
216
}
217
218
return { triggerError };
219
}
220
221
test("error handling in act", () => {
222
const { result } = renderHook(() => useErrorHook());
223
224
expect(() => {
225
act(() => {
226
result.current.triggerError();
227
});
228
}).toThrow("Hook error occurred");
229
});
230
```