0
# State & Effects
1
2
Custom React hooks for lifecycle management, state synchronization, and optimized effects for React applications.
3
4
## Capabilities
5
6
### Update-Only Effects
7
8
Hooks that run effects only on updates, skipping the initial mount.
9
10
```typescript { .api }
11
/**
12
* useEffect that only runs on updates (skips initial mount)
13
* @param effect - Effect callback function
14
* @param deps - Dependency array (same as useEffect)
15
*/
16
function useUpdateEffect(effect: EffectCallback, deps?: DependencyList): void;
17
18
/**
19
* useLayoutEffect that only runs on updates (skips initial mount)
20
* @param effect - Effect callback function
21
* @param deps - Dependency array (same as useLayoutEffect)
22
*/
23
function useUpdateLayoutEffect(effect: EffectCallback, deps?: DependencyList): void;
24
25
type EffectCallback = () => void | (() => void | undefined);
26
type DependencyList = ReadonlyArray<any>;
27
```
28
29
**Usage Examples:**
30
31
```typescript
32
import { useUpdateEffect, useUpdateLayoutEffect } from "@react-aria/utils";
33
34
function ComponentWithUpdateEffects({ value, onValueChange }) {
35
const [internalValue, setInternalValue] = useState(value);
36
37
// Only run when value changes, not on initial mount
38
useUpdateEffect(() => {
39
console.log('Value changed from external source');
40
onValueChange(value);
41
}, [value, onValueChange]);
42
43
// Layout effect that skips initial mount
44
useUpdateLayoutEffect(() => {
45
// Adjust layout when internal value changes
46
const element = document.getElementById('value-display');
47
if (element) {
48
element.style.width = `${internalValue.toString().length * 10}px`;
49
}
50
}, [internalValue]);
51
52
return (
53
<div>
54
<div id="value-display">{internalValue}</div>
55
<input
56
value={internalValue}
57
onChange={(e) => setInternalValue(e.target.value)}
58
/>
59
</div>
60
);
61
}
62
63
// Form validation that only runs on changes
64
function ValidatedInput({ value, onValidation, ...props }) {
65
const [isValid, setIsValid] = useState(true);
66
const [errorMessage, setErrorMessage] = useState('');
67
68
// Skip validation on initial mount, only validate on changes
69
useUpdateEffect(() => {
70
const validate = async () => {
71
try {
72
await validateValue(value);
73
setIsValid(true);
74
setErrorMessage('');
75
onValidation(true, '');
76
} catch (error) {
77
setIsValid(false);
78
setErrorMessage(error.message);
79
onValidation(false, error.message);
80
}
81
};
82
83
validate();
84
}, [value, onValidation]);
85
86
return (
87
<div>
88
<input
89
{...props}
90
value={value}
91
className={isValid ? '' : 'error'}
92
/>
93
{!isValid && <span className="error-message">{errorMessage}</span>}
94
</div>
95
);
96
}
97
```
98
99
### Cross-Platform Layout Effects
100
101
Hook providing consistent useLayoutEffect behavior across environments.
102
103
```typescript { .api }
104
/**
105
* Cross-platform useLayoutEffect (useEffect in SSR environments)
106
* Prevents hydration mismatches by using useEffect during SSR
107
*/
108
const useLayoutEffect: typeof React.useLayoutEffect;
109
```
110
111
**Usage Examples:**
112
113
```typescript
114
import { useLayoutEffect } from "@react-aria/utils";
115
116
function ResponsiveComponent() {
117
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
118
const elementRef = useRef<HTMLDivElement>(null);
119
120
// Safe to use in SSR - will use useEffect during server rendering
121
useLayoutEffect(() => {
122
if (elementRef.current) {
123
const { offsetWidth, offsetHeight } = elementRef.current;
124
setDimensions({ width: offsetWidth, height: offsetHeight });
125
}
126
}, []);
127
128
return (
129
<div ref={elementRef}>
130
Dimensions: {dimensions.width} x {dimensions.height}
131
</div>
132
);
133
}
134
135
// DOM measurement that needs to be synchronous
136
function SynchronousMeasurement({ children }) {
137
const containerRef = useRef<HTMLDivElement>(null);
138
const [containerHeight, setContainerHeight] = useState(0);
139
140
useLayoutEffect(() => {
141
if (containerRef.current) {
142
// This measurement happens synchronously before paint
143
const height = containerRef.current.scrollHeight;
144
setContainerHeight(height);
145
146
// Adjust other elements based on measurement
147
document.body.style.setProperty('--container-height', `${height}px`);
148
}
149
});
150
151
return (
152
<div ref={containerRef} style={{ minHeight: containerHeight }}>
153
{children}
154
</div>
155
);
156
}
157
```
158
159
### Value State Management
160
161
Hook for managing values that can transition through generator functions.
162
163
```typescript { .api }
164
/**
165
* Manages values that can transition through generator functions
166
* @param defaultValue - Initial value
167
* @returns Tuple of current value and setter function
168
*/
169
function useValueEffect<T>(defaultValue: T): [T, (value: T | (() => Generator<T>)) => void];
170
```
171
172
**Usage Examples:**
173
174
```typescript
175
import { useValueEffect } from "@react-aria/utils";
176
177
function AnimatedCounter({ targetValue }) {
178
const [currentValue, setCurrentValue] = useValueEffect(0);
179
180
useEffect(() => {
181
// Use generator for smooth value transitions
182
setCurrentValue(function* () {
183
const start = currentValue;
184
const diff = targetValue - start;
185
const steps = 60; // 60 steps for smooth animation
186
187
for (let i = 0; i <= steps; i++) {
188
const progress = i / steps;
189
const easedProgress = 1 - Math.pow(1 - progress, 3); // ease-out
190
yield start + (diff * easedProgress);
191
}
192
193
return targetValue;
194
});
195
}, [targetValue, currentValue, setCurrentValue]);
196
197
return <div>Count: {Math.round(currentValue)}</div>;
198
}
199
200
// Color transition with generator
201
function ColorTransition({ targetColor }) {
202
const [currentColor, setCurrentColor] = useValueEffect({ r: 0, g: 0, b: 0 });
203
204
const transitionToColor = (newColor: { r: number; g: number; b: number }) => {
205
setCurrentColor(function* () {
206
const start = currentColor;
207
const steps = 30;
208
209
for (let i = 0; i <= steps; i++) {
210
const progress = i / steps;
211
212
yield {
213
r: Math.round(start.r + (newColor.r - start.r) * progress),
214
g: Math.round(start.g + (newColor.g - start.g) * progress),
215
b: Math.round(start.b + (newColor.b - start.b) * progress)
216
};
217
}
218
219
return newColor;
220
});
221
};
222
223
useEffect(() => {
224
transitionToColor(targetColor);
225
}, [targetColor]);
226
227
const colorString = `rgb(${currentColor.r}, ${currentColor.g}, ${currentColor.b})`;
228
229
return (
230
<div
231
style={{
232
backgroundColor: colorString,
233
width: 100,
234
height: 100,
235
transition: 'background-color 0.1s'
236
}}
237
/>
238
);
239
}
240
```
241
242
### Deep Memoization
243
244
Hook providing memoization with custom equality functions.
245
246
```typescript { .api }
247
/**
248
* Memoization with custom equality function
249
* @param value - Value to memoize
250
* @param isEqual - Function to compare values (optional)
251
* @returns Memoized value that only changes when equality check fails
252
*/
253
function useDeepMemo<T>(value: T, isEqual?: (a: T, b: T) => boolean): T;
254
```
255
256
**Usage Examples:**
257
258
```typescript
259
import { useDeepMemo } from "@react-aria/utils";
260
261
function DeepComparisonComponent({ items, options }) {
262
// Memoize complex objects with deep equality
263
const memoizedItems = useDeepMemo(items, (a, b) => {
264
if (a.length !== b.length) return false;
265
return a.every((item, index) =>
266
item.id === b[index].id && item.name === b[index].name
267
);
268
});
269
270
const memoizedOptions = useDeepMemo(options, (a, b) =>
271
JSON.stringify(a) === JSON.stringify(b)
272
);
273
274
// These will only recalculate when deep equality fails
275
const processedItems = useMemo(() =>
276
expensiveProcessing(memoizedItems)
277
, [memoizedItems]);
278
279
const computedOptions = useMemo(() =>
280
expensiveOptionProcessing(memoizedOptions)
281
, [memoizedOptions]);
282
283
return (
284
<div>
285
{processedItems.map(item => (
286
<div key={item.id}>{item.name}</div>
287
))}
288
</div>
289
);
290
}
291
292
// Custom equality for specific use cases
293
function useShallowEqual<T extends Record<string, any>>(value: T): T {
294
return useDeepMemo(value, (a, b) => {
295
const keysA = Object.keys(a);
296
const keysB = Object.keys(b);
297
298
if (keysA.length !== keysB.length) return false;
299
300
return keysA.every(key => a[key] === b[key]);
301
});
302
}
303
304
function OptimizedComponent({ config, data }) {
305
// Only re-render when top-level properties change
306
const stableConfig = useShallowEqual(config);
307
const stableData = useShallowEqual(data);
308
309
const result = useMemo(() => {
310
return processData(stableData, stableConfig);
311
}, [stableData, stableConfig]);
312
313
return <div>{result.summary}</div>;
314
}
315
```
316
317
### Form Reset Handling
318
319
Hook for handling form reset events and restoring initial values.
320
321
```typescript { .api }
322
/**
323
* Handles form reset events
324
* @param ref - RefObject to form element or form control
325
* @param initialValue - Value to reset to
326
* @param onReset - Callback when reset occurs
327
*/
328
function useFormReset<T>(
329
ref: RefObject<HTMLFormElement | HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>,
330
initialValue: T,
331
onReset: (value: T) => void
332
): void;
333
```
334
335
**Usage Examples:**
336
337
```typescript
338
import { useFormReset } from "@react-aria/utils";
339
340
function ControlledInput({ name, initialValue = '', onValueChange }) {
341
const [value, setValue] = useState(initialValue);
342
const inputRef = useRef<HTMLInputElement>(null);
343
344
// Handle form reset events
345
useFormReset(inputRef, initialValue, (resetValue) => {
346
setValue(resetValue);
347
onValueChange(resetValue);
348
});
349
350
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
351
const newValue = e.target.value;
352
setValue(newValue);
353
onValueChange(newValue);
354
};
355
356
return (
357
<input
358
ref={inputRef}
359
name={name}
360
value={value}
361
onChange={handleChange}
362
/>
363
);
364
}
365
366
// Complex form with multiple controlled components
367
function ComplexForm() {
368
const formRef = useRef<HTMLFormElement>(null);
369
const [formData, setFormData] = useState({
370
name: '',
371
email: '',
372
preferences: { notifications: true, theme: 'light' }
373
});
374
375
const initialFormData = {
376
name: '',
377
email: '',
378
preferences: { notifications: true, theme: 'light' }
379
};
380
381
// Reset entire form state when form reset occurs
382
useFormReset(formRef, initialFormData, (resetData) => {
383
setFormData(resetData);
384
});
385
386
const updateFormData = (field: string, value: any) => {
387
setFormData(prev => ({ ...prev, [field]: value }));
388
};
389
390
return (
391
<form ref={formRef}>
392
<ControlledInput
393
name="name"
394
initialValue={initialFormData.name}
395
onValueChange={(value) => updateFormData('name', value)}
396
/>
397
398
<ControlledInput
399
name="email"
400
initialValue={initialFormData.email}
401
onValueChange={(value) => updateFormData('email', value)}
402
/>
403
404
<div>
405
<button type="submit">Submit</button>
406
<button type="reset">Reset Form</button>
407
</div>
408
</form>
409
);
410
}
411
```
412
413
### Advanced State Management Patterns
414
415
Complex scenarios combining multiple state and effect utilities:
416
417
```typescript
418
import {
419
useUpdateEffect,
420
useDeepMemo,
421
useValueEffect,
422
useFormReset
423
} from "@react-aria/utils";
424
425
function AdvancedStateManager({
426
externalData,
427
onDataChange,
428
resetTrigger
429
}) {
430
const formRef = useRef<HTMLFormElement>(null);
431
432
// Deep memo for expensive data processing
433
const processedData = useDeepMemo(externalData, (a, b) =>
434
JSON.stringify(a) === JSON.stringify(b)
435
);
436
437
// Value effect for smooth data transitions
438
const [displayData, setDisplayData] = useValueEffect(processedData);
439
440
// Update effect to sync with external changes
441
useUpdateEffect(() => {
442
// Animate to new data when external data changes
443
setDisplayData(function* () {
444
const steps = 20;
445
const start = displayData;
446
447
for (let i = 0; i <= steps; i++) {
448
const progress = i / steps;
449
// Interpolate between old and new data
450
yield interpolateData(start, processedData, progress);
451
}
452
453
return processedData;
454
});
455
}, [processedData]);
456
457
// Form reset handling
458
useFormReset(formRef, processedData, (resetData) => {
459
setDisplayData(resetData);
460
onDataChange(resetData);
461
});
462
463
return (
464
<form ref={formRef}>
465
<DataDisplay data={displayData} />
466
<button type="reset">Reset to Original</button>
467
</form>
468
);
469
}
470
471
// Performance-optimized component with multiple hooks
472
function PerformanceOptimizedComponent({
473
items,
474
filters,
475
sortOptions
476
}) {
477
// Memoize expensive computations
478
const memoizedItems = useDeepMemo(items);
479
const memoizedFilters = useDeepMemo(filters);
480
const memoizedSortOptions = useDeepMemo(sortOptions);
481
482
// Expensive filtering only when inputs actually change
483
const filteredItems = useMemo(() =>
484
filterItems(memoizedItems, memoizedFilters)
485
, [memoizedItems, memoizedFilters]);
486
487
// Sorting with smooth transitions
488
const [sortedItems, setSortedItems] = useValueEffect(filteredItems);
489
490
// Update sorted items when sort options change
491
useUpdateEffect(() => {
492
const newlySorted = sortItems(filteredItems, memoizedSortOptions);
493
494
// Animate sort changes
495
setSortedItems(function* () {
496
// Implement smooth reordering animation
497
yield* animateReorder(sortedItems, newlySorted);
498
return newlySorted;
499
});
500
}, [filteredItems, memoizedSortOptions]);
501
502
return (
503
<div>
504
{sortedItems.map(item => (
505
<ItemComponent key={item.id} item={item} />
506
))}
507
</div>
508
);
509
}
510
```
511
512
## Types
513
514
```typescript { .api }
515
type EffectCallback = () => void | (() => void | undefined);
516
type DependencyList = ReadonlyArray<any>;
517
518
interface MutableRefObject<T> {
519
current: T;
520
}
521
522
interface RefObject<T> {
523
readonly current: T | null;
524
}
525
```