React Development

The "Useless useCallback" Myth: 4 Secrets to Know 2025

Is `useCallback` useless? Debunk the myth in 2025. Uncover 4 secrets about referential equality, `React.memo`, dependency arrays, and when to avoid it.

A

Alex Ivanov

Senior React developer and performance enthusiast passionate about writing clean, efficient code.

6 min read8 views

The Great `useCallback` Debate

If you've spent any time in the React ecosystem, you've witnessed the endless debate: is `useCallback` a vital performance tool or a premature optimization that clutters your code? Articles and senior developers often warn against its overuse, leading many to brand it as "useless" or a code smell. But what if the conventional wisdom is missing the point?

The truth, as we'll explore in 2025, is that `useCallback` is one of the most misunderstood hooks in React. Its purpose isn't to magically speed up your app or stop functions from being "re-created"—a cheap operation in modern JavaScript engines. Its true power lies in a core computer science concept: referential equality.

Forget the myths. We're about to uncover the four secrets that separate junior from senior React developers, transforming `useCallback` from a confusing hook into a precision tool in your performance arsenal.

Secret #1: Unleashing the True Power of `React.memo`

This is the most common and critical use case for `useCallback`. To understand it, you must first understand that `React.memo` performs a shallow comparison of a component's props. If the props from the last render are `===` to the props of the current render, React skips re-rendering the component.

The Re-render Cascade: A Silent Performance Killer

What happens when you pass a function as a prop? In JavaScript, functions are objects. This means every time a parent component renders, any function defined within its body is a brand new function in memory.

Consider this:

const fn1 = () => {};
const fn2 = () => {};
console.log(fn1 === fn2); // false!

Even though they look identical, they have different memory references. So, if you pass a plain function to a memoized child component, the prop check will always fail, and your expensive child component will re-render unnecessarily on every parent render. `React.memo` becomes completely ineffective.

Code in Action: A Practical `React.memo` and `useCallback` Example

Let's see it in action. Here's a `UserProfile` component that we want to memoize because it might be computationally expensive.

// Child Component
const UserProfile = React.memo(({ onUpdateName }) => {
  console.log('Rendering UserProfile...');
  return <button onClick={onUpdateName}>Update Name</button>;
});

// Parent Component
function Dashboard() {
  const [user, setUser] = useState({ name: 'Alex' });
  const [theme, setTheme] = useState('dark');

  // PROBLEM: This function is new on every render
  const handleUpdateName = () => {
    setUser({ name: 'Alex ' + Math.random() });
  };

  return (
    <div>
      <button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>
        Toggle Theme
      </button>
      <UserProfile onUpdateName={handleUpdateName} />
    </div>
  );
}

In this example, every time you click "Toggle Theme," the `Dashboard` component re-renders. This creates a new `handleUpdateName` function, causing `UserProfile` to re-render despite its props not *functionally* changing. You'll see "Rendering UserProfile..." in the console every time.

Now, let's fix it with `useCallback`:

// ...inside Dashboard component
const handleUpdateName = useCallback(() => {
  setUser({ name: 'Alex ' + Math.random() });
}, []); // Empty dependency array means this function is created only once

// ...rest of the component is the same

With this change, `useCallback` ensures that `handleUpdateName` maintains the same memory reference across renders. Now, when you toggle the theme, `Dashboard` re-renders, but since `onUpdateName` is referentially stable, `React.memo` works its magic, and `UserProfile` does not re-render. You've successfully stopped a needless render cycle.

Secret #2: Taming the Dependency Array Beast

The second critical role of `useCallback` is to stabilize functions used within the dependency arrays of other hooks, like `useEffect`, `useMemo`, and even other `useCallback` hooks.

Why Unstable Functions Cause Effect Storms

A dependency array tells a hook, "Only re-run if one of these values has changed." Just like with `React.memo`, this comparison is based on referential equality. If you place a function that's redefined on every render into a dependency array, you're creating an infinite loop or causing an effect to run far more often than needed.

Imagine a `useEffect` that fetches data based on a user's ID and uses a callback function:

function UserData({ userId }) {
  const [data, setData] = useState(null);

  // DANGER: fetchData is a new function on every render
  const fetchData = () => {
    console.log('Fetching data for', userId);
    // api.fetch(`/users/${userId}`).then(setData);
  };

  useEffect(() => {
    fetchData();
  }, [userId, fetchData]); // fetchData will always trigger this effect

  return <div>{/* ... display data ... */} </div>;
}

In this flawed example, any parent component re-render will cause `UserData` to re-render, creating a new `fetchData` function. The `useEffect` sees this new function, thinks a dependency has changed, and runs the fetch again, even if `userId` is the same. This can lead to a cascade of unnecessary API calls.

A Real-World Scenario: Fetching Data with Stable Callbacks

The solution is to memoize the callback, ensuring it only changes when its *own* dependencies change.

function UserData({ userId }) {
  const [data, setData] = useState(null);

  // CORRECT: The function is stable unless userId changes
  const fetchData = useCallback(() => {
    console.log('Fetching data for', userId);
    // api.fetch(`/users/${userId}`).then(setData);
  }, [userId]); // Now it's properly dependent on userId

  useEffect(() => {
    fetchData();
  }, [fetchData]); // This effect now only runs when fetchData is re-memoized

  return <div>{/* ... display data ... */} </div>;
}

By wrapping `fetchData` in `useCallback` and listing `userId` as its dependency, we create a stable reference. The `fetchData` function is only re-created if `userId` changes, which is exactly the behavior we want. Our `useEffect` is now predictable and efficient.

Secret #3: Crafting Bulletproof Custom Hooks

If you write custom hooks that return functions, `useCallback` is not just a good idea—it's essential for creating a robust and predictable API for other developers.

The Stable API Contract

When a developer uses your custom hook, they expect its return values to be stable unless something has genuinely changed. If your hook returns a new function on every render, you force the consumer of your hook to deal with the problems we outlined in Secret #2. They can't reliably use your returned function in dependency arrays without causing unwanted side effects.

Example: Building a `useToggle` Hook the Right Way

Let's build a simple `useToggle` hook. Here's the naive version:

// Naive implementation - BAD
const useToggle = (initialState = false) => {
  const [state, setState] = useState(initialState);
  
  // This 'toggle' function is a new object on every render
  const toggle = () => setState(s => !s);

  return [state, toggle];
};

If a component uses this hook and puts `toggle` in a `useEffect` dependency array, that effect will run on every render of the component. This breaks the principle of least surprise.

Here's the professional, stable version using `useCallback`:

// Robust implementation - GOOD
const useToggle = (initialState = false) => {
  const [state, setState] = useState(initialState);

  // 'toggle' is now referentially stable
  const toggle = useCallback(() => {
    setState(s => !s);
  }, []); // No dependencies, so it's created only once

  return [state, toggle];
};

By returning a memoized `toggle` function, you provide a stable API. Developers using your `useToggle` hook can now safely use the `toggle` function in dependency arrays without fear of triggering infinite loops or unnecessary re-renders.

Secret #4: The Performance Tax & When to Say No

So, should we wrap every function in `useCallback`? Absolutely not. This is where the "useless useCallback" myth originates. Using it indiscriminately is a classic sign of premature optimization and actually adds a small but real cost.

The Overhead of Memoization

`useCallback` is not free. On each render, React must:

  1. Call the `useCallback` hook itself.
  2. Create and store the memoized function.
  3. Compare the dependency array with the previous render's array.

This overhead is tiny, but it's greater than zero. More importantly, it adds cognitive overhead—your code becomes more verbose and harder to read. If a function isn't being used in a way that requires referential stability, adding `useCallback` is worse than doing nothing.

Use this table as your guide:

When to Use `useCallback` vs. a Plain Function
ScenarioShould you use `useCallback`?Rationale
Function passed to a `React.memo`-wrapped component.Yes, absolutely.Prevents breaking memoization by providing a stable prop reference.
Function used in a dependency array (`useEffect`, `useMemo`).Yes, critically important.Prevents infinite loops or unnecessary hook re-runs.
Function returned from a custom hook.Yes, for a stable API.Allows consumers of your hook to use the function in their own dependency arrays safely.
Simple event handler on a native HTML element (e.g., `<div onClick={...}>`).No, almost never.There's no `React.memo` to break or dependency array to stabilize. The overhead outweighs the benefit.

Conclusion: From "Useless" to Precision Tool

The `useCallback` hook is far from useless. It's a specialized tool for a specific job: preserving referential equality to control re-renders. By understanding its deep connection with `React.memo`, dependency arrays, and custom hook design, you can wield it effectively.

Stop thinking of `useCallback` as a way to prevent function re-creation. Start thinking of it as a way to provide a stable identity for your functions when React's rendering system needs it. Master these four secrets, and you'll write cleaner, more performant, and more professional React applications in 2025 and beyond.