0
# Event Management
1
2
Cross-platform event handling with automatic cleanup and stable function references for React components.
3
4
## Capabilities
5
6
### useEvent Hook
7
8
Attaches event listeners to elements referenced by ref with automatic cleanup.
9
10
```typescript { .api }
11
/**
12
* Attaches event listeners to elements referenced by ref
13
* @param ref - RefObject pointing to target element
14
* @param event - Event type to listen for
15
* @param handler - Event handler function (optional)
16
* @param options - addEventListener options
17
*/
18
function useEvent<K extends keyof GlobalEventHandlersEventMap>(
19
ref: RefObject<EventTarget | null>,
20
event: K,
21
handler?: (this: Document, ev: GlobalEventHandlersEventMap[K]) => any,
22
options?: AddEventListenerOptions
23
): void;
24
```
25
26
**Usage Examples:**
27
28
```typescript
29
import { useEvent } from "@react-aria/utils";
30
import { useRef } from "react";
31
32
function InteractiveElement() {
33
const elementRef = useRef<HTMLDivElement>(null);
34
35
// Attach multiple event listeners with automatic cleanup
36
useEvent(elementRef, 'mouseenter', (e) => {
37
console.log('Mouse entered', e.target);
38
});
39
40
useEvent(elementRef, 'mouseleave', (e) => {
41
console.log('Mouse left', e.target);
42
});
43
44
useEvent(elementRef, 'keydown', (e) => {
45
if (e.key === 'Enter') {
46
console.log('Enter pressed');
47
}
48
}, { passive: false });
49
50
return (
51
<div ref={elementRef} tabIndex={0}>
52
Hover me or press Enter
53
</div>
54
);
55
}
56
57
// Conditional event listeners
58
function ConditionalEvents({ isActive }) {
59
const buttonRef = useRef<HTMLButtonElement>(null);
60
61
// Handler only attached when isActive is true
62
useEvent(
63
buttonRef,
64
'click',
65
isActive ? (e) => console.log('Button clicked') : undefined
66
);
67
68
return (
69
<button ref={buttonRef}>
70
{isActive ? 'Active Button' : 'Inactive Button'}
71
</button>
72
);
73
}
74
```
75
76
### useGlobalListeners Hook
77
78
Manages global event listeners with automatic cleanup and proper handling.
79
80
```typescript { .api }
81
/**
82
* Manages global event listeners with automatic cleanup
83
* @returns Object with methods for managing global listeners
84
*/
85
function useGlobalListeners(): GlobalListeners;
86
87
interface GlobalListeners {
88
addGlobalListener<K extends keyof DocumentEventMap>(
89
el: EventTarget,
90
type: K,
91
listener: (this: Document, ev: DocumentEventMap[K]) => any,
92
options?: AddEventListenerOptions | boolean
93
): void;
94
95
removeGlobalListener<K extends keyof DocumentEventMap>(
96
el: EventTarget,
97
type: K,
98
listener: (this: Document, ev: DocumentEventMap[K]) => any,
99
options?: EventListenerOptions | boolean
100
): void;
101
102
removeAllGlobalListeners(): void;
103
}
104
```
105
106
**Usage Examples:**
107
108
```typescript
109
import { useGlobalListeners } from "@react-aria/utils";
110
111
function GlobalEventComponent() {
112
const { addGlobalListener, removeGlobalListener } = useGlobalListeners();
113
114
useEffect(() => {
115
const handleGlobalClick = (e) => {
116
console.log('Global click detected');
117
};
118
119
const handleGlobalKeyDown = (e) => {
120
if (e.key === 'Escape') {
121
console.log('Escape pressed globally');
122
}
123
};
124
125
// Add global listeners
126
addGlobalListener(document, 'click', handleGlobalClick);
127
addGlobalListener(document, 'keydown', handleGlobalKeyDown);
128
129
// Manual cleanup (automatic cleanup happens on unmount)
130
return () => {
131
removeGlobalListener(document, 'click', handleGlobalClick);
132
removeGlobalListener(document, 'keydown', handleGlobalKeyDown);
133
};
134
}, [addGlobalListener, removeGlobalListener]);
135
136
return <div>Component with global event listeners</div>;
137
}
138
139
// Modal overlay with outside click detection
140
function ModalOverlay({ isOpen, onClose, children }) {
141
const { addGlobalListener, removeGlobalListener } = useGlobalListeners();
142
const overlayRef = useRef<HTMLDivElement>(null);
143
144
useEffect(() => {
145
if (!isOpen) return;
146
147
const handleOutsideClick = (e) => {
148
if (overlayRef.current && !overlayRef.current.contains(e.target)) {
149
onClose();
150
}
151
};
152
153
const handleEscape = (e) => {
154
if (e.key === 'Escape') {
155
onClose();
156
}
157
};
158
159
// Add listeners when modal opens
160
addGlobalListener(document, 'mousedown', handleOutsideClick);
161
addGlobalListener(document, 'keydown', handleEscape);
162
163
return () => {
164
removeGlobalListener(document, 'mousedown', handleOutsideClick);
165
removeGlobalListener(document, 'keydown', handleEscape);
166
};
167
}, [isOpen, onClose, addGlobalListener, removeGlobalListener]);
168
169
return isOpen ? (
170
<div className="modal-backdrop">
171
<div ref={overlayRef} className="modal-content">
172
{children}
173
</div>
174
</div>
175
) : null;
176
}
177
```
178
179
### useEffectEvent Hook
180
181
Creates a stable function reference that always calls the latest version, preventing unnecessary effect re-runs.
182
183
```typescript { .api }
184
/**
185
* Creates a stable function reference that always calls the latest version
186
* @param fn - Function to wrap
187
* @returns Stable function reference
188
*/
189
function useEffectEvent<T extends Function>(fn?: T): T;
190
```
191
192
**Usage Examples:**
193
194
```typescript
195
import { useEffectEvent } from "@react-aria/utils";
196
197
function SearchComponent({ query, onResults }) {
198
// Stable reference to callback that doesn't cause effect re-runs
199
const handleResults = useEffectEvent(onResults);
200
201
useEffect(() => {
202
if (!query) return;
203
204
const searchAPI = async () => {
205
const results = await fetch(`/api/search?q=${query}`);
206
const data = await results.json();
207
208
// This won't cause the effect to re-run when onResults changes
209
handleResults(data);
210
};
211
212
searchAPI();
213
}, [query]); // Only re-run when query changes, not when onResults changes
214
215
return <div>Searching for: {query}</div>;
216
}
217
218
// Event handler that accesses latest state without dependencies
219
function Timer() {
220
const [count, setCount] = useState(0);
221
const [isRunning, setIsRunning] = useState(false);
222
223
// Stable reference that always accesses latest state
224
const tick = useEffectEvent(() => {
225
if (isRunning) {
226
setCount(c => c + 1);
227
}
228
});
229
230
useEffect(() => {
231
const interval = setInterval(tick, 1000);
232
return () => clearInterval(interval);
233
}, []); // No dependencies needed because tick is stable
234
235
return (
236
<div>
237
<div>Count: {count}</div>
238
<button onClick={() => setIsRunning(!isRunning)}>
239
{isRunning ? 'Stop' : 'Start'}
240
</button>
241
</div>
242
);
243
}
244
```
245
246
### Advanced Event Management Patterns
247
248
Complex event handling scenarios with multiple listeners and cleanup:
249
250
```typescript
251
import { useGlobalListeners, useEffectEvent, useEvent } from "@react-aria/utils";
252
253
function AdvancedEventManager({ onAction, isActive }) {
254
const elementRef = useRef<HTMLDivElement>(null);
255
const { addGlobalListener, removeGlobalListener } = useGlobalListeners();
256
257
// Stable callback reference
258
const handleAction = useEffectEvent(onAction);
259
260
// Local element events
261
useEvent(elementRef, 'click', isActive ? (e) => {
262
handleAction('local-click', e);
263
} : undefined);
264
265
useEvent(elementRef, 'keydown', (e) => {
266
if (e.key === 'Enter' || e.key === ' ') {
267
e.preventDefault();
268
handleAction('local-activate', e);
269
}
270
});
271
272
// Global events with conditional handling
273
useEffect(() => {
274
if (!isActive) return;
275
276
const handleGlobalKeydown = (e) => {
277
// Handle global shortcuts
278
if (e.ctrlKey && e.key === 'k') {
279
e.preventDefault();
280
handleAction('global-search', e);
281
}
282
};
283
284
const handleGlobalWheel = (e) => {
285
// Custom scroll handling
286
if (e.deltaY > 0) {
287
handleAction('scroll-down', e);
288
} else {
289
handleAction('scroll-up', e);
290
}
291
};
292
293
addGlobalListener(document, 'keydown', handleGlobalKeydown);
294
addGlobalListener(document, 'wheel', handleGlobalWheel, { passive: true });
295
296
return () => {
297
removeGlobalListener(document, 'keydown', handleGlobalKeydown);
298
removeGlobalListener(document, 'wheel', handleGlobalWheel);
299
};
300
}, [isActive, addGlobalListener, removeGlobalListener, handleAction]);
301
302
return (
303
<div ref={elementRef} tabIndex={0}>
304
Advanced Event Manager
305
</div>
306
);
307
}
308
309
// Drag and drop with event management
310
function DragDropZone({ onDrop }) {
311
const zoneRef = useRef<HTMLDivElement>(null);
312
const { addGlobalListener, removeGlobalListener } = useGlobalListeners();
313
const [isDragging, setIsDragging] = useState(false);
314
315
const handleDrop = useEffectEvent(onDrop);
316
317
// Local drag events
318
useEvent(zoneRef, 'dragover', (e) => {
319
e.preventDefault();
320
setIsDragging(true);
321
});
322
323
useEvent(zoneRef, 'dragleave', (e) => {
324
if (!zoneRef.current?.contains(e.relatedTarget as Node)) {
325
setIsDragging(false);
326
}
327
});
328
329
useEvent(zoneRef, 'drop', (e) => {
330
e.preventDefault();
331
setIsDragging(false);
332
handleDrop(e.dataTransfer?.files);
333
});
334
335
// Global drag cleanup
336
useEffect(() => {
337
if (!isDragging) return;
338
339
const handleGlobalDragEnd = () => {
340
setIsDragging(false);
341
};
342
343
addGlobalListener(document, 'dragend', handleGlobalDragEnd);
344
return () => removeGlobalListener(document, 'dragend', handleGlobalDragEnd);
345
}, [isDragging, addGlobalListener, removeGlobalListener]);
346
347
return (
348
<div
349
ref={zoneRef}
350
className={isDragging ? 'drag-over' : ''}
351
>
352
Drop files here
353
</div>
354
);
355
}
356
```
357
358
## Types
359
360
```typescript { .api }
361
interface AddEventListenerOptions {
362
capture?: boolean;
363
once?: boolean;
364
passive?: boolean;
365
signal?: AbortSignal;
366
}
367
368
interface EventListenerOptions {
369
capture?: boolean;
370
}
371
372
interface GlobalEventHandlersEventMap {
373
click: MouseEvent;
374
keydown: KeyboardEvent;
375
keyup: KeyboardEvent;
376
mousedown: MouseEvent;
377
mouseup: MouseEvent;
378
mousemove: MouseEvent;
379
wheel: WheelEvent;
380
// ... other global event types
381
}
382
383
interface DocumentEventMap extends GlobalEventHandlersEventMap {
384
// Document-specific events
385
}
386
```