Not using useCallback when passing event handlers to child components

Loading

The Importance of useCallback for Event Handlers in Child Components

A common React performance issue occurs when passing event handlers to child components without proper memoization using useCallback. This can lead to unnecessary re-renders and degraded performance.

The Problem

function ParentComponent() {
  const [count, setCount] = useState(0);

  // ❌ New function created on every render
  const handleClick = () => {
    setCount(count + 1);
  };

  return <ChildComponent onClick={handleClick} />;
}

const ChildComponent = React.memo(({ onClick }) => {
  return <button onClick={onClick}>Click me</button>;
});

Why this is problematic:

  1. Creates new function reference on each render
  2. Causes child component to re-render unnecessarily
  3. Defeats the purpose of React.memo
  4. Can lead to performance bottlenecks in large apps

Correct Solution with useCallback

function ParentComponent() {
  const [count, setCount] = useState(0);

  // ✅ Memoized callback with dependencies
  const handleClick = useCallback(() => {
    setCount(prevCount => prevCount + 1);
  }, []); // Empty array = never changes

  return <ChildComponent onClick={handleClick} />;
}

const ChildComponent = React.memo(({ onClick }) => {
  return <button onClick={onClick}>Click me</button>;
});

Key Improvements

  1. Stable function reference – Same function between re-renders
  2. Proper dependency handling – Only recreates when dependencies change
  3. Functional updates – Ensures fresh state without adding count to deps
  4. Memoized childReact.memo can now prevent unnecessary re-renders

When to Use useCallback

  1. Passing callbacks to optimized child components (React.memo)
  2. When the callback is used in dependency arrays of other hooks
  3. For expensive calculations in callbacks
  4. When the callback is passed through multiple component layers

Common Mistakes to Avoid

  1. Missing dependencies:
   const [count, setCount] = useState(0);

   // ❌ Missing count dependency
   const handleClick = useCallback(() => {
     setCount(count + 1);
   }, []);
  1. Unnecessary memoization:
   // ❌ Over-optimization for simple components
   const handleClick = useCallback(() => console.log('click'), []);
  1. Incorrect child optimization:
   // ❌ Child not memoized - callback memoization wasted
   const ChildComponent = ({ onClick }) => { ... };

Performance Considerations

  1. Shallow comparisonReact.memo compares props by reference
  2. Memory overheaduseCallback has slight memory cost
  3. CPU tradeoff – Recreating functions vs. comparing props

Advanced Patterns

1. Dynamic Event Handlers

function ParentComponent({ items }) {
  const handleItemClick = useCallback((itemId) => {
    console.log('Item clicked:', itemId);
  }, []);

  return items.map(item => (
    <MemoizedChild 
      key={item.id}
      item={item}
      onClick={handleItemClick}
    />
  ));
}

2. Context with Callbacks

const AppContext = createContext();

function AppProvider({ children }) {
  const [state, setState] = useState();

  // ✅ Memoized context value
  const value = useMemo(() => ({
    state,
    update: useCallback((newState) => {
      setState(prev => ({ ...prev, ...newState }));
    }, [])
  }), [state]);

  return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
}

3. Custom Hook with Callbacks

function useCounter(initialValue = 0) {
  const [count, setCount] = useState(initialValue);

  const increment = useCallback(() => {
    setCount(c => c + 1);
  }, []);

  const decrement = useCallback(() => {
    setCount(c => c - 1);
  }, []);

  return { count, increment, decrement };
}

When Not to Use useCallback

  1. Simple components with no performance issues
  2. Callbacks that don’t cause child re-renders
  3. Cases where the overhead outweighs benefits
  4. When the callback changes frequently anyway

Leave a Reply

Your email address will not be published. Required fields are marked *