CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-react-aria--utils

Essential utility functions and React hooks for building accessible React Aria UI components

Pending

Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

Overview
Eval results
Files

state-and-effects.mddocs/

State & Effects

Custom React hooks for lifecycle management, state synchronization, and optimized effects for React applications.

Capabilities

Update-Only Effects

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>
  );
}

Cross-Platform Layout Effects

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>
  );
}

Value State Management

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'
      }}
    />
  );
}

Deep Memoization

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>;
}

Form Reset Handling

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>
  );
}

Advanced State Management Patterns

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>
  );
}

Types

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

docs

animation-and-transitions.md

event-management.md

focus-and-accessibility.md

id-and-refs.md

index.md

links-and-navigation.md

miscellaneous-utilities.md

platform-detection.md

props-and-events.md

scrolling-and-layout.md

shadow-dom-support.md

state-and-effects.md

virtual-events-and-input.md

tile.json