BeginnerReact Concepts

useCallback in React – Stop Recreating the Same Function 100 Times

useCallback memoizes a function so it doesn’t get recreated on every render — helping you optimize performance and prevent re-renders.

By Rudraksh Laddha

Last week, I was optimizing a dashboard with 50+ chart components in Virendana Ui. Performance was terrible — every small state update caused the entire dashboard to re-render. The culprit? I was creating new event handler functions on every render, causing React.memo to be completely useless.

"Why is this child component re-rendering? The props look identical in React DevTools..."

The answer is function reference equality. Every render creates new function objects, even if they do the same thing. That's where useCallback becomes essential.

What useCallback Actually Solves

useCallback returns a memoized version of a callback function that only changes when its dependencies change. Think of it as React's way of saying "keep using this same function until I tell you otherwise."

The key insight: In JavaScript, functions are objects, and {} !== {} even if they're identical. React sees new function = new prop = re-render needed.

The Basic Pattern

const memoizedCallback = useCallback(() => {
  // Your function logic here
}, [dependency1, dependency2]);

React only recreates the function when values in the dependency array change. Empty array means the function never changes.

Real-World Example: Optimizing List Performance

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

  // Without useCallback - creates new function every render
  // const handleToggle = (id) => {
  //   setTodos(prev => prev.map(todo => 
  //     todo.id === id ? { ...todo, completed: !todo.completed } : todo
  //   ));
  // };

  // With useCallback - function stays stable
  const handleToggle = useCallback((id) => {
    setTodos(prev => prev.map(todo => 
      todo.id === id ? { ...todo, completed: !todo.completed } : todo
    ));
  }, []); // Empty deps - function never changes

  const handleDelete = useCallback((id) => {
    setTodos(prev => prev.filter(todo => todo.id !== id));
  }, []);

  return (
    <div>
      {todos.map(todo => (
        <TodoItem 
          key={todo.id}
          todo={todo}
          onToggle={handleToggle}
          onDelete={handleDelete}
        />
      ))}
    </div>
  );
};

const TodoItem = React.memo(({ todo, onToggle, onDelete }) => {
  console.log('TodoItem rendered:', todo.id);
  
  return (
    <div>
      <span>{todo.text}</span>
      <button onClick={() => onToggle(todo.id)}>Toggle</button>
      <button onClick={() => onDelete(todo.id)}>Delete</button>
    </div>
  );
});

Without useCallback, every filter change would re-render every TodoItem. With it, only the filtered items re-render. In a 1000-item list, this makes the difference between sluggish and smooth.

The Anti-Pattern: Memoizing Everything

// DON'T DO THIS - Unnecessary memoization
const handleClick = useCallback(() => {
  console.log('clicked');
}, []); // This function is only used in this component

return <button onClick={handleClick}>Click me</button>;

This is wasteful. useCallback has overhead — you're trading memory for performance that doesn't exist. Only memoize functions that are passed as props to memoized components or used in effect dependencies.

When I Reach for useCallback

After years of React development, I use useCallback in these specific scenarios:

  • Props to React.memo components — The most common and effective use case
  • useEffect dependencies — Prevents infinite re-runs when effects depend on functions
  • Context values — Prevents all consumers from re-rendering unnecessarily
  • Large lists with event handlers — Each item gets the same function reference

useCallback vs useMemo: Know the Difference

Aspect useCallback useMemo
What it memoizes The function itself The result of calling a function
Returns Same function reference Computed value
Best for Event handlers, callbacks Expensive calculations
Example use onClick handlers for list items Filtering/sorting large datasets

Performance Traps I've Fallen Into

Premature optimization is the root of all evil — this applies to useCallback too.

Common mistakes I've made (and debugged):

  • Over-memoizing simple functions — The memoization overhead exceeds the benefit
  • Wrong dependencies — Function recreates on every render anyway
  • Memoizing without React.memo — The child component isn't optimized to skip renders
  • Forgetting exhaustive-deps — ESLint will save you here

Advanced Pattern: useCallback with useEffect

One of the most valuable patterns I use regularly — preventing infinite effect loops:

const UserProfile = ({ userId }) => {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(false);

  // Memoize the fetch function to prevent effect re-runs
  const fetchUser = useCallback(async () => {
    setLoading(true);
    try {
      const response = await fetch(`/api/users/${userId}`);
      const userData = await response.json();
      setUser(userData);
    } catch (error) {
      console.error('Failed to fetch user:', error);
    } finally {
      setLoading(false);
    }
  }, [userId]); // Only recreate when userId changes

  useEffect(() => {
    fetchUser();
  }, [fetchUser]); // Safe to include in deps now

  // ... rest of component
};

Without useCallback, the effect would run on every render because fetchUser is a new function each time. This pattern is essential for data fetching with proper dependency management.

useCallback Mastery Guide

Concept Key Insight
Primary purpose Stabilize function references between renders
Performance benefit Prevents unnecessary child re-renders
When to use Props to memoized components, effect dependencies
When to avoid Internal functions, premature optimization
Best practices Pair with React.memo, trust ESLint exhaustive-deps

useCallback Questions from Code Reviews

When should I use useCallback instead of useMemo?

Use useCallback for memoizing functions themselves, useMemo for memoizing computed values. If you're passing a function as a prop or using it in useEffect dependencies, useCallback is your tool.

Does useCallback actually improve performance?

Only when used correctly with memoized components. Without React.memo on the receiving component, useCallback provides no performance benefit and adds overhead.

Should I memoize every function I pass as a prop?

No. Profile first, optimize second. Most React apps don't need this level of optimization. Focus on components that actually have performance issues.

What happens if I forget dependencies in useCallback?

The function will use stale values from when it was first created. Always use the exhaustive-deps ESLint rule — it catches these bugs before they reach production.

Can useCallback cause memory leaks?

Rarely, but possible if you're capturing large objects in closures. The memoized function keeps references to all variables in its scope. Be mindful of what you're closing over.

❤️ At Learn Virendana, we love creating high-quality React tutorials that simplify complex concepts and deliver a practical, real-world React learning experience for developers