0
# Miscellaneous Utilities
1
2
Additional utilities for DOM helpers, value management, constants, and utility functions that support React Aria components.
3
4
## Capabilities
5
6
### DOM Helpers
7
8
Utilities for working with DOM elements and their properties.
9
10
```typescript { .api }
11
/**
12
* Gets element's offset position
13
* @param element - Target element
14
* @param reverse - Whether to measure from right/bottom (default: false)
15
* @param orientation - Measurement direction (default: 'horizontal')
16
* @returns Offset value in pixels
17
*/
18
function getOffset(
19
element: Element,
20
reverse?: boolean,
21
orientation?: "horizontal" | "vertical"
22
): number;
23
24
/**
25
* Gets the document that owns an element
26
* @param el - Element to get document for
27
* @returns Element's owner document or global document
28
*/
29
function getOwnerDocument(el: Element): Document;
30
31
/**
32
* Gets the window that owns an element
33
* @param el - Element to get window for
34
* @returns Element's owner window or global window
35
*/
36
function getOwnerWindow(el: Element): Window;
37
38
/**
39
* Type guard to check if node is a ShadowRoot
40
* @param node - Node to check
41
* @returns Boolean with proper type narrowing
42
*/
43
function isShadowRoot(node: Node): node is ShadowRoot;
44
```
45
46
**Usage Examples:**
47
48
```typescript
49
import { getOffset, getOwnerDocument, getOwnerWindow, isShadowRoot } from "@react-aria/utils";
50
51
function PositionTracker({ children }) {
52
const elementRef = useRef<HTMLDivElement>(null);
53
const [position, setPosition] = useState({ x: 0, y: 0 });
54
55
useEffect(() => {
56
if (!elementRef.current) return;
57
58
const updatePosition = () => {
59
const element = elementRef.current!;
60
61
// Get horizontal and vertical offsets
62
const x = getOffset(element, false, 'horizontal');
63
const y = getOffset(element, false, 'vertical');
64
65
setPosition({ x, y });
66
};
67
68
// Get the appropriate window for event listeners
69
const ownerWindow = getOwnerWindow(elementRef.current);
70
71
updatePosition();
72
ownerWindow.addEventListener('scroll', updatePosition);
73
ownerWindow.addEventListener('resize', updatePosition);
74
75
return () => {
76
ownerWindow.removeEventListener('scroll', updatePosition);
77
ownerWindow.removeEventListener('resize', updatePosition);
78
};
79
}, []);
80
81
return (
82
<div ref={elementRef}>
83
<p>Position: ({position.x}, {position.y})</p>
84
{children}
85
</div>
86
);
87
}
88
89
// Cross-frame DOM utilities
90
function CrossFrameComponent({ targetFrame }) {
91
const [targetDocument, setTargetDocument] = useState<Document | null>(null);
92
93
useEffect(() => {
94
if (targetFrame && targetFrame.contentDocument) {
95
const frameDoc = targetFrame.contentDocument;
96
setTargetDocument(frameDoc);
97
98
// Use owner document utilities for frame elements
99
const frameElements = frameDoc.querySelectorAll('.interactive');
100
frameElements.forEach(element => {
101
const ownerDoc = getOwnerDocument(element);
102
const ownerWin = getOwnerWindow(element);
103
104
console.log('Element owner document:', ownerDoc === frameDoc);
105
console.log('Element owner window:', ownerWin === targetFrame.contentWindow);
106
});
107
}
108
}, [targetFrame]);
109
110
return <div>Cross-frame utilities active</div>;
111
}
112
113
// Shadow DOM detection
114
function ShadowDOMHandler({ rootElement }) {
115
useEffect(() => {
116
if (!rootElement) return;
117
118
const walker = document.createTreeWalker(
119
rootElement,
120
NodeFilter.SHOW_ELEMENT,
121
{
122
acceptNode: (node) => {
123
// Check if we've encountered a shadow root
124
if (isShadowRoot(node)) {
125
console.log('Found shadow root:', node);
126
return NodeFilter.FILTER_ACCEPT;
127
}
128
return NodeFilter.FILTER_SKIP;
129
}
130
}
131
);
132
133
let currentNode;
134
while (currentNode = walker.nextNode()) {
135
// Process shadow roots
136
handleShadowRoot(currentNode as ShadowRoot);
137
}
138
}, [rootElement]);
139
140
return null;
141
}
142
```
143
144
### Value Management
145
146
Utilities for managing values and preventing unnecessary re-renders.
147
148
```typescript { .api }
149
/**
150
* Creates an inert reference to a value
151
* @param value - Value to make inert
152
* @returns Inert value reference that doesn't trigger re-renders
153
*/
154
function inertValue<T>(value: T): T;
155
```
156
157
**Usage Examples:**
158
159
```typescript
160
import { inertValue } from "@react-aria/utils";
161
162
function ExpensiveComponent({ data, onProcess }) {
163
// Create inert reference to prevent unnecessary recalculations
164
const inertData = inertValue(data);
165
166
const processedData = useMemo(() => {
167
// Expensive processing that should only run when data actually changes
168
return expensiveProcessing(inertData);
169
}, [inertData]);
170
171
return <div>{processedData.summary}</div>;
172
}
173
174
// Stable callback references
175
function CallbackComponent({ onChange }) {
176
// Make callback inert to prevent effect dependencies
177
const inertOnChange = inertValue(onChange);
178
179
useEffect(() => {
180
// This effect won't re-run when onChange reference changes
181
const subscription = subscribe(inertOnChange);
182
return () => subscription.unsubscribe();
183
}, [inertOnChange]);
184
185
return <div>Component with stable callback</div>;
186
}
187
```
188
189
### Constants
190
191
Pre-defined constants used by React Aria components.
192
193
```typescript { .api }
194
/**
195
* Custom event name for clearing focus in autocomplete
196
*/
197
const CLEAR_FOCUS_EVENT = "react-aria-clear-focus";
198
199
/**
200
* Custom event name for setting focus in autocomplete
201
*/
202
const FOCUS_EVENT = "react-aria-focus";
203
```
204
205
**Usage Examples:**
206
207
```typescript
208
import { CLEAR_FOCUS_EVENT, FOCUS_EVENT } from "@react-aria/utils";
209
210
function CustomAutocomplete({ suggestions, onSelect }) {
211
const inputRef = useRef<HTMLInputElement>(null);
212
const listRef = useRef<HTMLUListElement>(null);
213
214
const clearFocus = () => {
215
// Dispatch custom clear focus event
216
const event = new CustomEvent(CLEAR_FOCUS_EVENT, {
217
bubbles: true,
218
detail: { target: inputRef.current }
219
});
220
221
inputRef.current?.dispatchEvent(event);
222
};
223
224
const focusOption = (optionElement: HTMLElement) => {
225
// Dispatch custom focus event
226
const event = new CustomEvent(FOCUS_EVENT, {
227
bubbles: true,
228
detail: { target: optionElement }
229
});
230
231
optionElement.dispatchEvent(event);
232
};
233
234
useEffect(() => {
235
const input = inputRef.current;
236
if (!input) return;
237
238
const handleClearFocus = (e: CustomEvent) => {
239
console.log('Clear focus requested:', e.detail.target);
240
input.blur();
241
};
242
243
const handleFocus = (e: CustomEvent) => {
244
console.log('Focus requested:', e.detail.target);
245
e.detail.target.focus();
246
};
247
248
input.addEventListener(CLEAR_FOCUS_EVENT, handleClearFocus);
249
listRef.current?.addEventListener(FOCUS_EVENT, handleFocus);
250
251
return () => {
252
input.removeEventListener(CLEAR_FOCUS_EVENT, handleClearFocus);
253
listRef.current?.removeEventListener(FOCUS_EVENT, handleFocus);
254
};
255
}, []);
256
257
return (
258
<div>
259
<input ref={inputRef} />
260
<ul ref={listRef}>
261
{suggestions.map((suggestion, index) => (
262
<li
263
key={index}
264
onClick={() => focusOption(inputRef.current!)}
265
>
266
{suggestion}
267
</li>
268
))}
269
</ul>
270
<button onClick={clearFocus}>Clear Focus</button>
271
</div>
272
);
273
}
274
```
275
276
### Drag & Drop (Deprecated)
277
278
One-dimensional drag gesture utility (deprecated in favor of newer alternatives).
279
280
```typescript { .api }
281
/**
282
* ⚠️ DEPRECATED - Use @react-aria/interactions useMove instead
283
* 1D dragging behavior for sliders and similar components
284
* @param props - Configuration for drag behavior
285
* @returns Event handlers for drag functionality
286
*/
287
function useDrag1D(props: {
288
containerRef?: RefObject<Element>;
289
reverse?: boolean;
290
orientation?: "horizontal" | "vertical";
291
onDrag?: (e: { deltaX: number; deltaY: number }) => void;
292
onDragStart?: (e: PointerEvent) => void;
293
onDragEnd?: (e: PointerEvent) => void;
294
}): HTMLAttributes<HTMLElement>;
295
```
296
297
**Usage Examples:**
298
299
```typescript
300
import { useDrag1D } from "@react-aria/utils";
301
302
// ⚠️ This is deprecated - use @react-aria/interactions instead
303
function DeprecatedSlider({ value, onChange, min = 0, max = 100 }) {
304
const containerRef = useRef<HTMLDivElement>(null);
305
306
const dragProps = useDrag1D({
307
containerRef,
308
orientation: 'horizontal',
309
onDrag: ({ deltaX }) => {
310
const container = containerRef.current;
311
if (!container) return;
312
313
const containerWidth = container.offsetWidth;
314
const deltaValue = (deltaX / containerWidth) * (max - min);
315
const newValue = Math.max(min, Math.min(max, value + deltaValue));
316
317
onChange(newValue);
318
}
319
});
320
321
return (
322
<div ref={containerRef} className="slider" {...dragProps}>
323
<div className="slider-track">
324
<div
325
className="slider-thumb"
326
style={{ left: `${((value - min) / (max - min)) * 100}%` }}
327
/>
328
</div>
329
</div>
330
);
331
}
332
```
333
334
### Load More Utilities
335
336
Utilities for implementing infinite scrolling and pagination.
337
338
```typescript { .api }
339
/**
340
* Manages infinite scrolling and load more functionality
341
* @param options - Configuration for load more behavior
342
* @returns Load more state and controls
343
*/
344
function useLoadMore<T>(options: {
345
items: T[];
346
onLoadMore: () => Promise<T[]> | void;
347
isLoading?: boolean;
348
hasMore?: boolean;
349
}): {
350
items: T[];
351
isLoading: boolean;
352
hasMore: boolean;
353
loadMore: () => void;
354
};
355
356
/**
357
* Uses IntersectionObserver to trigger load more when sentinel is visible
358
* @param props - Configuration for sentinel behavior
359
* @param ref - RefObject to sentinel element
360
*/
361
function useLoadMoreSentinel(
362
props: LoadMoreSentinelProps,
363
ref: RefObject<HTMLElement>
364
): void;
365
366
/**
367
* Unstable version of useLoadMoreSentinel
368
* @deprecated Use useLoadMoreSentinel instead
369
*/
370
const UNSTABLE_useLoadMoreSentinel = useLoadMoreSentinel;
371
372
interface LoadMoreSentinelProps {
373
collection: any;
374
onLoadMore: () => void;
375
scrollOffset?: number;
376
}
377
```
378
379
**Usage Examples:**
380
381
```typescript
382
import { useLoadMore, useLoadMoreSentinel } from "@react-aria/utils";
383
384
function InfiniteList({ initialItems, loadMoreItems }) {
385
const {
386
items,
387
isLoading,
388
hasMore,
389
loadMore
390
} = useLoadMore({
391
items: initialItems,
392
onLoadMore: async () => {
393
const newItems = await loadMoreItems();
394
return newItems;
395
},
396
hasMore: true
397
});
398
399
const sentinelRef = useRef<HTMLDivElement>(null);
400
401
useLoadMoreSentinel({
402
collection: items,
403
onLoadMore: loadMore,
404
scrollOffset: 0.8 // Trigger when 80% scrolled
405
}, sentinelRef);
406
407
return (
408
<div className="infinite-list">
409
{items.map(item => (
410
<div key={item.id}>{item.name}</div>
411
))}
412
413
{hasMore && (
414
<div ref={sentinelRef} className="loading-sentinel">
415
{isLoading ? 'Loading...' : 'Load more'}
416
</div>
417
)}
418
</div>
419
);
420
}
421
```
422
423
### Re-exported Utilities
424
425
Mathematical utilities re-exported from @react-stately/utils.
426
427
```typescript { .api }
428
/**
429
* Clamps value between min and max bounds
430
* @param value - Value to clamp
431
* @param min - Minimum value
432
* @param max - Maximum value
433
* @returns Clamped value
434
*/
435
function clamp(value: number, min: number, max: number): number;
436
437
/**
438
* Snaps value to nearest step increment
439
* @param value - Value to snap
440
* @param step - Step increment
441
* @param min - Optional minimum value
442
* @returns Snapped value
443
*/
444
function snapValueToStep(value: number, step: number, min?: number): number;
445
```
446
447
**Usage Examples:**
448
449
```typescript
450
import { clamp, snapValueToStep } from "@react-aria/utils";
451
452
function NumericInput({ value, onChange, min = 0, max = 100, step = 1 }) {
453
const handleChange = (newValue: number) => {
454
// Clamp to bounds and snap to step
455
const clampedValue = clamp(newValue, min, max);
456
const snappedValue = snapValueToStep(clampedValue, step, min);
457
458
onChange(snappedValue);
459
};
460
461
return (
462
<input
463
type="range"
464
value={value}
465
min={min}
466
max={max}
467
step={step}
468
onChange={(e) => handleChange(Number(e.target.value))}
469
/>
470
);
471
}
472
473
// Color picker with HSL snapping
474
function ColorPicker({ hue, saturation, lightness, onChange }) {
475
const handleHueChange = (newHue: number) => {
476
// Snap hue to 15-degree increments
477
const snappedHue = snapValueToStep(newHue, 15);
478
const clampedHue = clamp(snappedHue, 0, 360);
479
480
onChange({ hue: clampedHue, saturation, lightness });
481
};
482
483
const handleSaturationChange = (newSaturation: number) => {
484
// Snap saturation to 5% increments
485
const snappedSat = snapValueToStep(newSaturation, 5);
486
const clampedSat = clamp(snappedSat, 0, 100);
487
488
onChange({ hue, saturation: clampedSat, lightness });
489
};
490
491
return (
492
<div>
493
<input
494
type="range"
495
min={0}
496
max={360}
497
step={15}
498
value={hue}
499
onChange={(e) => handleHueChange(Number(e.target.value))}
500
/>
501
<input
502
type="range"
503
min={0}
504
max={100}
505
step={5}
506
value={saturation}
507
onChange={(e) => handleSaturationChange(Number(e.target.value))}
508
/>
509
</div>
510
);
511
}
512
513
// Animation easing with step snapping
514
function useAnimationValue(targetValue: number, duration = 1000) {
515
const [currentValue, setCurrentValue] = useState(0);
516
517
useEffect(() => {
518
let startTime: number;
519
let animationFrame: number;
520
521
const animate = (timestamp: number) => {
522
if (!startTime) startTime = timestamp;
523
524
const progress = clamp((timestamp - startTime) / duration, 0, 1);
525
526
// Ease-out function
527
const easedProgress = 1 - Math.pow(1 - progress, 3);
528
529
// Calculate intermediate value and snap to steps
530
const intermediateValue = easedProgress * targetValue;
531
const snappedValue = snapValueToStep(intermediateValue, 0.1);
532
533
setCurrentValue(snappedValue);
534
535
if (progress < 1) {
536
animationFrame = requestAnimationFrame(animate);
537
}
538
};
539
540
animationFrame = requestAnimationFrame(animate);
541
542
return () => {
543
if (animationFrame) {
544
cancelAnimationFrame(animationFrame);
545
}
546
};
547
}, [targetValue, duration]);
548
549
return currentValue;
550
}
551
```
552
553
### Utility Combinations
554
555
Real-world examples combining multiple miscellaneous utilities:
556
557
```typescript
558
import {
559
getOffset,
560
getOwnerWindow,
561
clamp,
562
snapValueToStep,
563
inertValue
564
} from "@react-aria/utils";
565
566
function DraggableSlider({ value, onChange, min = 0, max = 100, step = 1 }) {
567
const sliderRef = useRef<HTMLDivElement>(null);
568
const [isDragging, setIsDragging] = useState(false);
569
570
// Make onChange inert to prevent unnecessary effect re-runs
571
const inertOnChange = inertValue(onChange);
572
573
const handleMouseMove = useCallback((e: MouseEvent) => {
574
if (!isDragging || !sliderRef.current) return;
575
576
// Get slider position and dimensions
577
const slider = sliderRef.current;
578
const sliderRect = slider.getBoundingClientRect();
579
const sliderOffset = getOffset(slider, false, 'horizontal');
580
581
// Calculate relative position
582
const relativeX = e.clientX - sliderRect.left;
583
const percentage = clamp(relativeX / sliderRect.width, 0, 1);
584
585
// Convert to value and snap to step
586
const rawValue = min + percentage * (max - min);
587
const snappedValue = snapValueToStep(rawValue, step, min);
588
const finalValue = clamp(snappedValue, min, max);
589
590
inertOnChange(finalValue);
591
}, [isDragging, min, max, step, inertOnChange]);
592
593
useEffect(() => {
594
if (!isDragging) return;
595
596
const ownerWindow = sliderRef.current
597
? getOwnerWindow(sliderRef.current)
598
: window;
599
600
ownerWindow.addEventListener('mousemove', handleMouseMove);
601
ownerWindow.addEventListener('mouseup', () => setIsDragging(false));
602
603
return () => {
604
ownerWindow.removeEventListener('mousemove', handleMouseMove);
605
ownerWindow.removeEventListener('mouseup', () => setIsDragging(false));
606
};
607
}, [isDragging, handleMouseMove]);
608
609
const handlePercent = clamp((value - min) / (max - min), 0, 1);
610
611
return (
612
<div
613
ref={sliderRef}
614
className="slider"
615
onMouseDown={() => setIsDragging(true)}
616
>
617
<div
618
className="slider-thumb"
619
style={{ left: `${handlePercent * 100}%` }}
620
/>
621
</div>
622
);
623
}
624
```