Back to knowledges

React Optimization Hooks

Published: Sep 18, 2025
By: Pedro Henrique Braga de Castro

First, it's important to understand what Memoization is.

It's not a React concept. It's bigger. It's a browser concept and it's used to cache some information and improve optimization by recording previous information.

There are 3 hooks in React that are specialized for optimization.

useMemo()

state-level

This hook avoids expensive calculations being triggered on every render. The main point here is to memoize the calculation result.

Benefits

  • Prevents expensive recalculations
  • Synchronous operation only
  • Perfect for processing data
  • Improves render performance

Disadvantages

  • Cannot be used in async functions
  • Adds memory overhead
  • Can be overused incorrectly
  • Only worth it for expensive operations

Trade-off

Performance gains vs Memory overhead + complexity

useCallback()

function-level

Memoizes functions passed as props to components memoized with React.memo. Dependencies array contains any state created outside its own scope.

Benefits

  • Prevents unnecessary re-renders
  • Optimizes memoized components
  • Stabilizes function references
  • Great for event handlers

Disadvantages

  • Only useful with React.memo
  • Dependency array complexity
  • Can create stale closures
  • Memory overhead for function storage

Trade-off

Stable function references vs Dependency management complexity

React.memo()

component-level

A component-level optimization that skips re-rendering if the props haven't changed. Memoization happens at the render result level.

Benefits

  • Component-level optimization
  • Automatic shallow comparison
  • Easy to implement
  • Prevents unnecessary renders

Disadvantages

  • Only shallow comparison by default
  • May need custom comparison function
  • Can be forgotten by developers
  • Not always worth the overhead

Trade-off

Prevented re-renders vs Comparison overhead

Understanding Shallow Comparison

All these hooks use shallow comparison to compare information. This means they only check if the reference has changed, not the deep content of objects or arrays. I will write a post about this concept as soon as possible.

Practical Examples

1. useMemo() - Expensive Calculations

// Without useMemo - recalculates on every render
function ExpensiveComponent({ items, filter }) {
  const expensiveValue = items
    .filter(item => item.category === filter)
    .reduce((sum, item) => sum + item.value, 0);
 
  return <div>Total: {expensiveValue}</div>;
}
 
// With useMemo - only recalculates when dependencies change
function OptimizedComponent({ items, filter }) {
  const expensiveValue = useMemo(() => {
    return items
      .filter(item => item.category === filter)
      .reduce((sum, item) => sum + item.value, 0);
  }, [items, filter]);
 
  return <div>Total: {expensiveValue}</div>;
}

2. useCallback() - Stable Function References

// Without useCallback - creates new function on every render
function Parent({ items }) {
  const [count, setCount] = useState(0);
  
  const handleClick = (id) => {
    // Some logic here
    console.log('Clicked:', id);
  };
 
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Count: {count}</button>
      {items.map(item => (
        <MemoizedChild key={item.id} item={item} onClick={handleClick} />
      ))}
    </div>
  );
}
 
// With useCallback - stable function reference
function OptimizedParent({ items }) {
  const [count, setCount] = useState(0);
  
  const handleClick = useCallback((id) => {
    console.log('Clicked:', id);
  }, []); // Empty dependency array since no external dependencies
 
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Count: {count}</button>
      {items.map(item => (
        <MemoizedChild key={item.id} item={item} onClick={handleClick} />
      ))}
    </div>
  );
}

3. React.memo() - Component Memoization

// Regular component - re-renders on every parent render
function RegularChild({ name, age }) {
  console.log('RegularChild rendered');
  return <div>{name} is {age} years old</div>;
}
 
// Memoized component - only re-renders when props change
const MemoizedChild = React.memo(function Child({ name, age }) {
  console.log('MemoizedChild rendered');
  return <div>{name} is {age} years old</div>;
});
 
// With custom comparison function
const CustomMemoChild = React.memo(function Child({ user, settings }) {
  return <div>{user.name} - Theme: {settings.theme}</div>;
}, (prevProps, nextProps) => {
  // Custom comparison logic
  return prevProps.user.id === nextProps.user.id &&
         prevProps.settings.theme === nextProps.settings.theme;
});

When to Use Each Hook

HookBest ForAvoid When
useMemo()Expensive calculations, complex data processingSimple calculations, async operations
useCallback()Functions passed to memoized componentsFunctions without memoized children
React.memo()Components that re-render frequently with same propsComponents that always receive new props

Common Pitfalls

The Dependency Array Trap: Always include all dependencies in your dependency arrays. Missing dependencies can cause stale closures and bugs.

Over-memoization: Not every function or calculation needs memoization. Sometimes the cure is worse than the disease.

Object Dependencies: Remember that {a: 1} !== {a: 1} in JavaScript. Consider using useMemo for object dependencies.

Conclusion

React's optimization hooks are powerful tools, but they're not magic bullets. The key is understanding when and how to use them effectively:

  • useMemo() for expensive calculations
  • useCallback() for stable function references
  • React.memo() for component-level optimization

Remember: Profile first, optimize second, and always measure the impact of your optimizations!