or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

async-operations.mddata-structures.mddom-interactions.mdeffects.mdindex.mdperformance.mdspecialized-hooks.mdstate-management.mdstorage.mdtimers.md
tile.json

performance.mddocs/

Performance

Optimization hooks for preventing unnecessary re-renders, managing expensive computations, and improving React application performance through memoization and reference stability.

Capabilities

Function Memoization

useMemoizedFn

Memoizes function to prevent unnecessary re-renders when function is passed as prop or dependency.

/**
 * Memoizes function to prevent unnecessary re-renders
 * Returns a stable function reference that persists across renders
 * @param fn - Function to memoize
 * @returns Memoized function with stable reference
 */
function useMemoizedFn<T extends noop>(fn: T): T;

type noop = (...args: any[]) => any;

Usage Example:

import { useMemoizedFn } from 'ahooks';
import { useState, useCallback, memo } from 'react';

// Child component that re-renders when props change
const ExpensiveChild = memo(({ onClick, data }: { onClick: () => void; data: any }) => {
  console.log('ExpensiveChild rendered');
  return (
    <div>
      <button onClick={onClick}>Click me</button>
      <p>Data: {JSON.stringify(data)}</p>
    </div>
  );
});

function ParentComponent() {
  const [count, setCount] = useState(0);
  const [data, setData] = useState({ value: 1 });
  
  // Without memoization - creates new function on each render
  const badClickHandler = () => {
    console.log('Button clicked', count);
    setCount(c => c + 1);
  };
  
  // With useCallback - requires dependencies array
  const okClickHandler = useCallback(() => {
    console.log('Button clicked', count);
    setCount(c => c + 1);
  }, [count]); // Re-creates when count changes
  
  // With useMemoizedFn - always stable, accesses latest values
  const goodClickHandler = useMemoizedFn(() => {
    console.log('Button clicked', count); // Always accesses latest count
    setCount(c => c + 1);
  });
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setData({ value: Math.random() })}>
        Update Data (triggers re-render)
      </button>
      
      {/* This will re-render every time due to new function reference */}
      <ExpensiveChild onClick={badClickHandler} data={data} />
      
      {/* This will re-render when count changes due to useCallback deps */}
      <ExpensiveChild onClick={okClickHandler} data={data} />
      
      {/* This will only re-render when data changes - optimal! */}
      <ExpensiveChild onClick={goodClickHandler} data={data} />
    </div>
  );
}

Event Handler Optimization:

import { useMemoizedFn } from 'ahooks';

function FormComponent() {
  const [formData, setFormData] = useState({
    name: '',
    email: '',
    phone: ''
  });
  
  // Memoized handlers that won't cause child re-renders
  const handleNameChange = useMemoizedFn((value: string) => {
    setFormData(prev => ({ ...prev, name: value }));
    // Can access other state/props without dependencies
    validateForm();
  });
  
  const handleEmailChange = useMemoizedFn((value: string) => {
    setFormData(prev => ({ ...prev, email: value }));
    validateForm();
  });
  
  const handleSubmit = useMemoizedFn(async () => {
    try {
      await submitForm(formData); // Always accesses latest formData
      resetForm();
    } catch (error) {
      console.error('Submit failed:', error);
    }
  });
  
  const validateForm = useMemoizedFn(() => {
    // Validation logic that accesses latest state
    return formData.name && formData.email;
  });
  
  return (
    <form>
      <FormField 
        label="Name"
        value={formData.name}
        onChange={handleNameChange} // Stable reference
      />
      <FormField 
        label="Email"
        value={formData.email}
        onChange={handleEmailChange} // Stable reference
      />
      <button type="button" onClick={handleSubmit}>
        Submit
      </button>
    </form>
  );
}

Reference Management

useLatest

Returns a mutable ref that always contains the latest value, useful for accessing current state in callbacks.

/**
 * Returns ref that always contains the latest value
 * Useful for accessing current state in async callbacks and intervals
 * @param value - Value to keep latest reference to
 * @returns Mutable ref object with current property
 */
function useLatest<T>(value: T): MutableRefObject<T>;

type MutableRefObject<T> = import('react').MutableRefObject<T>;

Usage Example:

import { useLatest } from 'ahooks';
import { useState, useEffect } from 'react';

function TimerComponent() {
  const [count, setCount] = useState(0);
  const [isRunning, setIsRunning] = useState(false);
  
  // Always contains the latest count value
  const latestCount = useLatest(count);
  const latestIsRunning = useLatest(isRunning);
  
  useEffect(() => {
    const interval = setInterval(() => {
      // Access latest state values in callback
      if (latestIsRunning.current) {
        console.log('Current count:', latestCount.current);
        setCount(c => c + 1);
        
        // Stop at 10
        if (latestCount.current >= 10) {
          setIsRunning(false);
        }
      }
    }, 1000);
    
    return () => clearInterval(interval);
  }, []); // No dependencies needed!
  
  // Async operation example
  const handleAsyncOperation = async () => {
    console.log('Starting async operation with count:', count);
    
    await new Promise(resolve => setTimeout(resolve, 2000));
    
    // After 2 seconds, count might have changed
    console.log('Count at start was:', count); // Stale value
    console.log('Current count is:', latestCount.current); // Latest value
    
    if (latestCount.current > 5) {
      alert('Count increased during async operation!');
    }
  };
  
  return (
    <div>
      <p>Count: {count}</p>
      <p>Status: {isRunning ? 'Running' : 'Stopped'}</p>
      
      <button onClick={() => setIsRunning(!isRunning)}>
        {isRunning ? 'Stop' : 'Start'} Timer
      </button>
      
      <button onClick={handleAsyncOperation}>
        Start Async Operation
      </button>
      
      <button onClick={() => setCount(0)}>Reset</button>
    </div>
  );
}

Computed Values

useCreation

Alternative to useMemo that supports dependency comparison function for more control over recalculation.

/**
 * Alternative to useMemo with custom dependency comparison
 * Useful when you need more control over when expensive computations run
 * @param factory - Function that returns computed value
 * @param deps - Dependency array for recalculation
 * @returns Computed value that only recalculates when dependencies change
 */
function useCreation<T>(factory: () => T, deps: DependencyList): T;

type DependencyList = ReadonlyArray<any>;

Usage Example:

import { useCreation } from 'ahooks';
import { useState } from 'react';

function ExpensiveComputationComponent() {
  const [items, setItems] = useState<number[]>([1, 2, 3, 4, 5]);
  const [filter, setFilter] = useState('all');
  const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('asc');
  
  // Expensive computation that only runs when dependencies change
  const processedItems = useCreation(() => {
    console.log('Expensive computation running...');
    
    // Simulate expensive operation
    const start = Date.now();
    while (Date.now() - start < 100) {} // Simulate 100ms work
    
    let result = [...items];
    
    // Apply filter
    if (filter === 'even') {
      result = result.filter(n => n % 2 === 0);
    } else if (filter === 'odd') {
      result = result.filter(n => n % 2 === 1);
    }
    
    // Apply sort
    result.sort((a, b) => sortOrder === 'asc' ? a - b : b - a);
    
    return result;
  }, [items, filter, sortOrder]);
  
  // Create expensive object instance only when needed
  const expensiveCalculator = useCreation(() => {
    console.log('Creating expensive calculator instance...');
    
    return {
      calculate: (nums: number[]) => {
        return nums.reduce((acc, num) => acc + Math.pow(num, 2), 0);
      },
      average: (nums: number[]) => {
        return nums.reduce((acc, num) => acc + num, 0) / nums.length;
      }
    };
  }, []); // Only create once
  
  const addRandomItem = () => {
    setItems(prev => [...prev, Math.floor(Math.random() * 100)]);
  };
  
  return (
    <div>
      <h2>Expensive Computation Example</h2>
      
      <div>
        <button onClick={addRandomItem}>Add Random Item</button>
        
        <select value={filter} onChange={(e) => setFilter(e.target.value)}>
          <option value="all">All Items</option>
          <option value="even">Even Items</option>
          <option value="odd">Odd Items</option>
        </select>
        
        <select value={sortOrder} onChange={(e) => setSortOrder(e.target.value as 'asc' | 'desc')}>
          <option value="asc">Ascending</option>
          <option value="desc">Descending</option>
        </select>
      </div>
      
      <div>
        <h3>Original Items:</h3>
        <p>{items.join(', ')}</p>
        
        <h3>Processed Items:</h3>
        <p>{processedItems.join(', ')}</p>
        
        <h3>Calculations:</h3>
        <p>Sum of squares: {expensiveCalculator.calculate(processedItems)}</p>
        <p>Average: {expensiveCalculator.average(processedItems).toFixed(2)}</p>
      </div>
    </div>
  );
}

Previous Values

usePrevious

Returns the previous value of a state or prop, useful for detecting changes and animations.

/**
 * Returns the previous value of a state or prop
 * @param state - Current value to track
 * @param shouldUpdate - Function to determine if previous value should update
 * @returns Previous value or undefined on first render
 */
function usePrevious<T>(state: T, shouldUpdate?: ShouldUpdateFunc<T>): T | undefined;

type ShouldUpdateFunc<T> = (prev?: T, next?: T) => boolean;

Usage Example:

import { usePrevious } from 'ahooks';
import { useState } from 'react';

function PreviousValueExample() {
  const [count, setCount] = useState(0);
  const [user, setUser] = useState({ id: 1, name: 'Alice', status: 'online' });
  
  // Track previous values
  const prevCount = usePrevious(count);
  const prevUser = usePrevious(user);
  
  // Conditional previous value tracking
  const prevUserStatus = usePrevious(user.status, (prev, next) => {
    // Only update previous when status actually changes
    return prev !== next;
  });
  
  // Calculate differences
  const countDifference = prevCount !== undefined ? count - prevCount : 0;
  
  // Detect changes
  const userChanged = prevUser && (
    prevUser.name !== user.name || 
    prevUser.status !== user.status
  );
  
  return (
    <div>
      <h2>Previous Value Tracking</h2>
      
      <div>
        <h3>Counter</h3>
        <p>Current: {count}</p>
        <p>Previous: {prevCount ?? 'N/A'}</p>
        <p>Difference: {countDifference > 0 ? '+' : ''}{countDifference}</p>
        
        <button onClick={() => setCount(c => c + 1)}>+1</button>
        <button onClick={() => setCount(c => c + 5)}>+5</button>
        <button onClick={() => setCount(c => c - 3)}>-3</button>
      </div>
      
      <div>
        <h3>User Info</h3>
        <p>Current: {user.name} ({user.status})</p>
        <p>Previous: {prevUser ? `${prevUser.name} (${prevUser.status})` : 'N/A'}</p>
        <p>Previous Status: {prevUserStatus ?? 'N/A'}</p>
        <p>User Changed: {userChanged ? 'Yes' : 'No'}</p>
        
        <button onClick={() => setUser(u => ({ ...u, name: 'Bob' }))}>
          Change Name to Bob
        </button>
        <button onClick={() => setUser(u => ({ ...u, status: u.status === 'online' ? 'offline' : 'online' }))}>
          Toggle Status
        </button>
        <button onClick={() => setUser(u => ({ ...u, id: u.id + 1 }))}>
          Change ID (no name/status change)
        </button>
      </div>
    </div>
  );
}

Animation and Transition Example:

import { usePrevious } from 'ahooks';
import { useState, useEffect } from 'react';

function AnimationExample() {
  const [position, setPosition] = useState({ x: 0, y: 0 });
  const [isAnimating, setIsAnimating] = useState(false);
  
  const prevPosition = usePrevious(position);
  
  // Detect position change and trigger animation
  useEffect(() => {
    if (prevPosition && (
      prevPosition.x !== position.x || 
      prevPosition.y !== position.y
    )) {
      setIsAnimating(true);
      const timer = setTimeout(() => setIsAnimating(false), 300);
      return () => clearTimeout(timer);
    }
  }, [position, prevPosition]);
  
  const moveRandom = () => {
    setPosition({
      x: Math.floor(Math.random() * 400),
      y: Math.floor(Math.random() * 200)
    });
  };
  
  return (
    <div style={{ position: 'relative', height: '300px', border: '1px solid #ccc' }}>
      <button onClick={moveRandom}>Move Random</button>
      
      <div
        style={{
          position: 'absolute',
          left: position.x,
          top: position.y + 50,
          width: '20px',
          height: '20px',
          backgroundColor: isAnimating ? 'red' : 'blue',
          borderRadius: '50%',
          transition: 'all 0.3s ease',
          transform: isAnimating ? 'scale(1.2)' : 'scale(1)'
        }}
      />
      
      <div style={{ position: 'absolute', bottom: '10px' }}>
        <p>Current: ({position.x}, {position.y})</p>
        <p>Previous: {prevPosition ? `(${prevPosition.x}, ${prevPosition.y})` : 'N/A'}</p>
        <p>Animating: {isAnimating ? 'Yes' : 'No'}</p>
      </div>
    </div>
  );
}

Performance Best Practices

Here's how these performance hooks work together for optimal React performance:

import { useMemoizedFn, useLatest, useCreation, usePrevious } from 'ahooks';
import { useState, memo } from 'react';

interface OptimizedComponentProps {
  items: Array<{ id: number; name: string; score: number }>;
  onItemClick: (id: number) => void;
  filterThreshold: number;
}

const OptimizedComponent = memo(({ items, onItemClick, filterThreshold }: OptimizedComponentProps) => {
  const [selectedId, setSelectedId] = useState<number | null>(null);
  
  // Memoized expensive computation
  const filteredAndSortedItems = useCreation(() => {
    console.log('Recomputing filtered items...');
    return items
      .filter(item => item.score >= filterThreshold)
      .sort((a, b) => b.score - a.score);
  }, [items, filterThreshold]);
  
  // Stable event handlers
  const handleItemSelect = useMemoizedFn((id: number) => {
    setSelectedId(id);
    onItemClick(id);
  });
  
  // Track changes for analytics
  const prevItemCount = usePrevious(filteredAndSortedItems.length);
  const itemCountChanged = prevItemCount !== undefined && prevItemCount !== filteredAndSortedItems.length;
  
  // Latest value access for async operations
  const latestSelection = useLatest(selectedId);
  
  const handleAsyncAction = useMemoizedFn(async () => {
    if (latestSelection.current) {
      await performAsyncAction(latestSelection.current);
    }
  });
  
  return (
    <div>
      <div>
        {itemCountChanged && (
          <p>Item count changed: {prevItemCount} → {filteredAndSortedItems.length}</p>
        )}
      </div>
      
      <ul>
        {filteredAndSortedItems.map(item => (
          <li 
            key={item.id}
            onClick={() => handleItemSelect(item.id)}
            style={{
              backgroundColor: selectedId === item.id ? 'lightblue' : 'transparent'
            }}
          >
            {item.name} - Score: {item.score}
          </li>
        ))}
      </ul>
      
      <button onClick={handleAsyncAction}>
        Perform Async Action
      </button>
    </div>
  );
});

Common Types

// React types
type MutableRefObject<T> = import('react').MutableRefObject<T>;
type DependencyList = ReadonlyArray<any>;

// Function type
type noop = (...args: any[]) => any;

// Update function type
type ShouldUpdateFunc<T> = (prev?: T, next?: T) => boolean;