Not using useCallback when passing event handlers to child components

Loading

The Importance of useCallback for Event Handlers in Child Components

When passing event handlers to child components in React, failing to use useCallback can lead to performance issues. Here’s what you need to know:

The Problem: Unnecessary Re-renders

Without useCallback:

function ParentComponent() {
  const handleClick = () => {
    console.log('Button clicked');
  };

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

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

What’s wrong?

  • A new handleClick function is created on every render
  • React.memo() optimization is defeated because props keep changing
  • Child component re-renders unnecessarily

The Solution: useCallback

function ParentComponent() {
  const handleClick = useCallback(() => {
    console.log('Button clicked');
  }, []); // Empty dependency array = same function

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

Why this works:

  • useCallback memoizes the function between renders
  • Same function reference is passed to child component
  • React.memo() optimization works as intended

When to Use useCallback

  1. Passing handlers to optimized children (React.memo, PureComponent)
  2. Handlers used in dependency arrays of other hooks
  3. Performance-sensitive components with frequent re-renders

Dependency Array Considerations

For handlers that depend on props/state:

function ParentComponent({ userId }) {
  const handleClick = useCallback(() => {
    console.log('User clicked:', userId);
  }, [userId]); // Recreate when userId changes

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

Common Mistakes to Avoid

Empty dependencies when values are needed:

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

// Bad - will always log initial count (0)
const handleClick = useCallback(() => {
  console.log(count);
}, []);

Correct with dependencies:

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

// Good - logs current count
const handleClick = useCallback(() => {
  console.log(count);
}, [count]);

Overusing useCallback everywhere:

// Unnecessary for simple components without performance issues
const handleClick = useCallback(() => {...}, []);

Performance Impact

ApproachFunction CreationChild Re-renders
No memoizationEvery renderEvery parent render
useCallbackWhen deps changeOnly when deps change

Real-World Example

function App() {
  const [todos, setTodos] = useState([]);
  const [filter, setFilter] = useState('all');

  // Stable callback - no dependencies
  const addTodo = useCallback((text) => {
    setTodos(prev => [...prev, { text, completed: false }]);
  }, []);

  // Depends on filter - recreates when filter changes
  const getVisibleTodos = useCallback(() => {
    return todos.filter(todo => 
      filter === 'all' || 
      (filter === 'completed' && todo.completed) ||
      (filter === 'active' && !todo.completed)
    );
  }, [todos, filter]);

  return (
    <div>
      <TodoForm onSubmit={addTodo} />
      <TodoList 
        todos={todos}
        getVisibleTodos={getVisibleTodos}
      />
      <FilterControls 
        currentFilter={filter}
        onFilterChange={setFilter}
      />
    </div>
  );
}

When You Can Skip useCallback

  1. Simple components that don’t re-render often
  2. Leaf components with no performance issues
  3. Cases where the cost of memoization outweighs benefits

Key Takeaways

  1. Use useCallback when passing handlers to optimized child components
  2. Include all dependencies in the dependency array
  3. Avoid overusing it where it doesn’t provide value
  4. Combine with React.memo for maximum performance benefits
  5. Profile your app to identify real performance bottlenecks

Proper use of useCallback helps prevent unnecessary re-renders and keeps your React application performant, especially in complex component trees.

Leave a Reply

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