Optimization hooks for preventing unnecessary re-renders, managing expensive computations, and improving React application performance through memoization and reference stability.
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>
);
}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>
);
}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>
);
}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>
);
}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>
);
});// 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;