Essential utility functions and React hooks for building accessible React Aria UI components
—
Quality
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Custom React hooks for lifecycle management, state synchronization, and optimized effects for React applications.
Hooks that run effects only on updates, skipping the initial mount.
/**
* useEffect that only runs on updates (skips initial mount)
* @param effect - Effect callback function
* @param deps - Dependency array (same as useEffect)
*/
function useUpdateEffect(effect: EffectCallback, deps?: DependencyList): void;
/**
* useLayoutEffect that only runs on updates (skips initial mount)
* @param effect - Effect callback function
* @param deps - Dependency array (same as useLayoutEffect)
*/
function useUpdateLayoutEffect(effect: EffectCallback, deps?: DependencyList): void;
type EffectCallback = () => void | (() => void | undefined);
type DependencyList = ReadonlyArray<any>;Usage Examples:
import { useUpdateEffect, useUpdateLayoutEffect } from "@react-aria/utils";
function ComponentWithUpdateEffects({ value, onValueChange }) {
const [internalValue, setInternalValue] = useState(value);
// Only run when value changes, not on initial mount
useUpdateEffect(() => {
console.log('Value changed from external source');
onValueChange(value);
}, [value, onValueChange]);
// Layout effect that skips initial mount
useUpdateLayoutEffect(() => {
// Adjust layout when internal value changes
const element = document.getElementById('value-display');
if (element) {
element.style.width = `${internalValue.toString().length * 10}px`;
}
}, [internalValue]);
return (
<div>
<div id="value-display">{internalValue}</div>
<input
value={internalValue}
onChange={(e) => setInternalValue(e.target.value)}
/>
</div>
);
}
// Form validation that only runs on changes
function ValidatedInput({ value, onValidation, ...props }) {
const [isValid, setIsValid] = useState(true);
const [errorMessage, setErrorMessage] = useState('');
// Skip validation on initial mount, only validate on changes
useUpdateEffect(() => {
const validate = async () => {
try {
await validateValue(value);
setIsValid(true);
setErrorMessage('');
onValidation(true, '');
} catch (error) {
setIsValid(false);
setErrorMessage(error.message);
onValidation(false, error.message);
}
};
validate();
}, [value, onValidation]);
return (
<div>
<input
{...props}
value={value}
className={isValid ? '' : 'error'}
/>
{!isValid && <span className="error-message">{errorMessage}</span>}
</div>
);
}Hook providing consistent useLayoutEffect behavior across environments.
/**
* Cross-platform useLayoutEffect (useEffect in SSR environments)
* Prevents hydration mismatches by using useEffect during SSR
*/
const useLayoutEffect: typeof React.useLayoutEffect;Usage Examples:
import { useLayoutEffect } from "@react-aria/utils";
function ResponsiveComponent() {
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
const elementRef = useRef<HTMLDivElement>(null);
// Safe to use in SSR - will use useEffect during server rendering
useLayoutEffect(() => {
if (elementRef.current) {
const { offsetWidth, offsetHeight } = elementRef.current;
setDimensions({ width: offsetWidth, height: offsetHeight });
}
}, []);
return (
<div ref={elementRef}>
Dimensions: {dimensions.width} x {dimensions.height}
</div>
);
}
// DOM measurement that needs to be synchronous
function SynchronousMeasurement({ children }) {
const containerRef = useRef<HTMLDivElement>(null);
const [containerHeight, setContainerHeight] = useState(0);
useLayoutEffect(() => {
if (containerRef.current) {
// This measurement happens synchronously before paint
const height = containerRef.current.scrollHeight;
setContainerHeight(height);
// Adjust other elements based on measurement
document.body.style.setProperty('--container-height', `${height}px`);
}
});
return (
<div ref={containerRef} style={{ minHeight: containerHeight }}>
{children}
</div>
);
}Hook for managing values that can transition through generator functions.
/**
* Manages values that can transition through generator functions
* @param defaultValue - Initial value
* @returns Tuple of current value and setter function
*/
function useValueEffect<T>(defaultValue: T): [T, (value: T | (() => Generator<T>)) => void];Usage Examples:
import { useValueEffect } from "@react-aria/utils";
function AnimatedCounter({ targetValue }) {
const [currentValue, setCurrentValue] = useValueEffect(0);
useEffect(() => {
// Use generator for smooth value transitions
setCurrentValue(function* () {
const start = currentValue;
const diff = targetValue - start;
const steps = 60; // 60 steps for smooth animation
for (let i = 0; i <= steps; i++) {
const progress = i / steps;
const easedProgress = 1 - Math.pow(1 - progress, 3); // ease-out
yield start + (diff * easedProgress);
}
return targetValue;
});
}, [targetValue, currentValue, setCurrentValue]);
return <div>Count: {Math.round(currentValue)}</div>;
}
// Color transition with generator
function ColorTransition({ targetColor }) {
const [currentColor, setCurrentColor] = useValueEffect({ r: 0, g: 0, b: 0 });
const transitionToColor = (newColor: { r: number; g: number; b: number }) => {
setCurrentColor(function* () {
const start = currentColor;
const steps = 30;
for (let i = 0; i <= steps; i++) {
const progress = i / steps;
yield {
r: Math.round(start.r + (newColor.r - start.r) * progress),
g: Math.round(start.g + (newColor.g - start.g) * progress),
b: Math.round(start.b + (newColor.b - start.b) * progress)
};
}
return newColor;
});
};
useEffect(() => {
transitionToColor(targetColor);
}, [targetColor]);
const colorString = `rgb(${currentColor.r}, ${currentColor.g}, ${currentColor.b})`;
return (
<div
style={{
backgroundColor: colorString,
width: 100,
height: 100,
transition: 'background-color 0.1s'
}}
/>
);
}Hook providing memoization with custom equality functions.
/**
* Memoization with custom equality function
* @param value - Value to memoize
* @param isEqual - Function to compare values (optional)
* @returns Memoized value that only changes when equality check fails
*/
function useDeepMemo<T>(value: T, isEqual?: (a: T, b: T) => boolean): T;Usage Examples:
import { useDeepMemo } from "@react-aria/utils";
function DeepComparisonComponent({ items, options }) {
// Memoize complex objects with deep equality
const memoizedItems = useDeepMemo(items, (a, b) => {
if (a.length !== b.length) return false;
return a.every((item, index) =>
item.id === b[index].id && item.name === b[index].name
);
});
const memoizedOptions = useDeepMemo(options, (a, b) =>
JSON.stringify(a) === JSON.stringify(b)
);
// These will only recalculate when deep equality fails
const processedItems = useMemo(() =>
expensiveProcessing(memoizedItems)
, [memoizedItems]);
const computedOptions = useMemo(() =>
expensiveOptionProcessing(memoizedOptions)
, [memoizedOptions]);
return (
<div>
{processedItems.map(item => (
<div key={item.id}>{item.name}</div>
))}
</div>
);
}
// Custom equality for specific use cases
function useShallowEqual<T extends Record<string, any>>(value: T): T {
return useDeepMemo(value, (a, b) => {
const keysA = Object.keys(a);
const keysB = Object.keys(b);
if (keysA.length !== keysB.length) return false;
return keysA.every(key => a[key] === b[key]);
});
}
function OptimizedComponent({ config, data }) {
// Only re-render when top-level properties change
const stableConfig = useShallowEqual(config);
const stableData = useShallowEqual(data);
const result = useMemo(() => {
return processData(stableData, stableConfig);
}, [stableData, stableConfig]);
return <div>{result.summary}</div>;
}Hook for handling form reset events and restoring initial values.
/**
* Handles form reset events
* @param ref - RefObject to form element or form control
* @param initialValue - Value to reset to
* @param onReset - Callback when reset occurs
*/
function useFormReset<T>(
ref: RefObject<HTMLFormElement | HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>,
initialValue: T,
onReset: (value: T) => void
): void;Usage Examples:
import { useFormReset } from "@react-aria/utils";
function ControlledInput({ name, initialValue = '', onValueChange }) {
const [value, setValue] = useState(initialValue);
const inputRef = useRef<HTMLInputElement>(null);
// Handle form reset events
useFormReset(inputRef, initialValue, (resetValue) => {
setValue(resetValue);
onValueChange(resetValue);
});
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
const newValue = e.target.value;
setValue(newValue);
onValueChange(newValue);
};
return (
<input
ref={inputRef}
name={name}
value={value}
onChange={handleChange}
/>
);
}
// Complex form with multiple controlled components
function ComplexForm() {
const formRef = useRef<HTMLFormElement>(null);
const [formData, setFormData] = useState({
name: '',
email: '',
preferences: { notifications: true, theme: 'light' }
});
const initialFormData = {
name: '',
email: '',
preferences: { notifications: true, theme: 'light' }
};
// Reset entire form state when form reset occurs
useFormReset(formRef, initialFormData, (resetData) => {
setFormData(resetData);
});
const updateFormData = (field: string, value: any) => {
setFormData(prev => ({ ...prev, [field]: value }));
};
return (
<form ref={formRef}>
<ControlledInput
name="name"
initialValue={initialFormData.name}
onValueChange={(value) => updateFormData('name', value)}
/>
<ControlledInput
name="email"
initialValue={initialFormData.email}
onValueChange={(value) => updateFormData('email', value)}
/>
<div>
<button type="submit">Submit</button>
<button type="reset">Reset Form</button>
</div>
</form>
);
}Complex scenarios combining multiple state and effect utilities:
import {
useUpdateEffect,
useDeepMemo,
useValueEffect,
useFormReset
} from "@react-aria/utils";
function AdvancedStateManager({
externalData,
onDataChange,
resetTrigger
}) {
const formRef = useRef<HTMLFormElement>(null);
// Deep memo for expensive data processing
const processedData = useDeepMemo(externalData, (a, b) =>
JSON.stringify(a) === JSON.stringify(b)
);
// Value effect for smooth data transitions
const [displayData, setDisplayData] = useValueEffect(processedData);
// Update effect to sync with external changes
useUpdateEffect(() => {
// Animate to new data when external data changes
setDisplayData(function* () {
const steps = 20;
const start = displayData;
for (let i = 0; i <= steps; i++) {
const progress = i / steps;
// Interpolate between old and new data
yield interpolateData(start, processedData, progress);
}
return processedData;
});
}, [processedData]);
// Form reset handling
useFormReset(formRef, processedData, (resetData) => {
setDisplayData(resetData);
onDataChange(resetData);
});
return (
<form ref={formRef}>
<DataDisplay data={displayData} />
<button type="reset">Reset to Original</button>
</form>
);
}
// Performance-optimized component with multiple hooks
function PerformanceOptimizedComponent({
items,
filters,
sortOptions
}) {
// Memoize expensive computations
const memoizedItems = useDeepMemo(items);
const memoizedFilters = useDeepMemo(filters);
const memoizedSortOptions = useDeepMemo(sortOptions);
// Expensive filtering only when inputs actually change
const filteredItems = useMemo(() =>
filterItems(memoizedItems, memoizedFilters)
, [memoizedItems, memoizedFilters]);
// Sorting with smooth transitions
const [sortedItems, setSortedItems] = useValueEffect(filteredItems);
// Update sorted items when sort options change
useUpdateEffect(() => {
const newlySorted = sortItems(filteredItems, memoizedSortOptions);
// Animate sort changes
setSortedItems(function* () {
// Implement smooth reordering animation
yield* animateReorder(sortedItems, newlySorted);
return newlySorted;
});
}, [filteredItems, memoizedSortOptions]);
return (
<div>
{sortedItems.map(item => (
<ItemComponent key={item.id} item={item} />
))}
</div>
);
}type EffectCallback = () => void | (() => void | undefined);
type DependencyList = ReadonlyArray<any>;
interface MutableRefObject<T> {
current: T;
}
interface RefObject<T> {
readonly current: T | null;
}Install with Tessl CLI
npx tessl i tessl/npm-react-aria--utils