Why Use useCallback Hook in React

This article explains the purpose of the useCallback hook in React, detailing how it optimizes application performance by memoizing function definitions. You will learn how useCallback prevents unnecessary child component re-renders, stabilizes dependency arrays in other hooks, and when you should (and should not) apply it in your codebase.


Understanding the Problem: Referential Equality

In React, every time a component re-renders, all functions declared inside it are recreated from scratch. This happens because JavaScript treats functions as first-class objects, meaning two functions with the exact same code are not considered equal if they point to different memory addresses (referential inequality).

// Each render creates a new instance of this function
const handleClick = () => {
  console.log("Clicked");
};

While recreating functions is fast and generally not a performance issue on its own, it causes problems when those functions are passed as props to optimized child components or used as dependencies in other hooks.


Key Reasons to Use useCallback

1. Preventing Unnecessary Re-renders of Child Components

When you pass a function as a prop to a child component, the child component detects a “new” prop on every render of the parent, even if the function’s logic hasn’t changed.

If the child component is wrapped in React.memo, it is supposed to skip re-rendering if its props remain the same. However, passing a freshly recreated function defeats React.memo entirely. Wrapping the function in useCallback ensures that the same function reference is passed across renders, allowing React.memo to work successfully.

import React, { useState, useCallback } from 'react';

// Child component wrapped in React.memo
const Button = React.memo(({ handleClick }) => {
  console.log("Button rendered");
  return <button onClick={handleClick}>Click me</button>;
});

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

  // useCallback keeps this function reference stable
  const increment = useCallback(() => {
    setCount((prevCount) => prevCount + 1);
  }, []); // Empty dependency array means the function reference never changes

  return (
    <div>
      <p>Count: {count}</p>
      <Button handleClick={increment} />
    </div>
  );
}

2. Stabilizing Hook Dependency Arrays

If you use a function inside a useEffect, useMemo, or another useCallback hook, you must include that function in the dependency array. If the function is recreated on every render, the dependent hook will run on every single render, potentially causing infinite loops or performance degradation.

Using useCallback ensures the function reference only changes when its own dependencies change, preventing unnecessary hook executions.

const fetchData = useCallback(() => {
  // Fetch logic using "userId"
  fetch(`https://api.example.com/user/${userId}`)
    .then(res => res.json());
}, [userId]); // Only updates when userId changes

useEffect(() => {
  fetchData();
}, [fetchData]); // Safely triggered only when fetchData reference changes

When Not to Use useCallback

It is a common mistake to wrap every single function in useCallback. Do not use it in the following scenarios: