React useCallback Hook

In the last lesson you learned about the React useMemo Hook, so you can cache a value and skip slow work. This lesson is about useCallback, which gives you back the same function between renders so a new function doesn’t trigger needless re-renders.

🤔 Why do we need useCallback?

Here’s the pain. A function written inside a component is a new object on every render, and React sometimes compares functions, so a new one looks like a change.

  • You pass a function down to a child as a prop, like onClick={handleClick}.
  • The parent re-renders, so handleClick is recreated as a brand new function.
  • A memoized child sees a “new” prop and re-renders too, even though nothing really changed.
  • Or an effect that depends on that function fires again, because the function “changed”.

So useCallback keeps a function’s identity stable. It says “give me back the same function as last time, unless one of these dependencies changed”.

🐢 The problem in code

Here a parent passes handleClick to a child wrapped in React.memo. Even so, the child re-renders every time the parent does, because the function is new each render.

import { useState, memo } from "react";
const Child = memo(function Child({ onClick }) {
console.log("Child rendered"); // shows how often the child renders
return <button onClick={onClick}>Click me</button>;
});
function Parent() {
const [count, setCount] = useState(0);
// ❌ a brand new function on every Parent render
function handleClick() {
console.log("clicked");
}
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Add</button>
<Child onClick={handleClick} />
</div>
);
}

Walk through why the child keeps re-rendering.

  • React.memo makes Child skip re-rendering when its props are the same as before.
  • But handleClick is recreated on every Parent render, so it’s a different function each time.
  • Child compares the old onClick to the new one, sees they differ, and re-renders.
  • So the memo did nothing, because the function prop changed on every render.

Output

Child rendered
(user clicks Add)
Child rendered
(user clicks Add)
Child rendered

🧩 The fix: wrap the function in useCallback

Now we keep the same function across renders by wrapping handleClick in useCallback with a dependency array, just like useMemo.

import { useState, useCallback, memo } from "react";
function Parent() {
const [count, setCount] = useState(0);
// ✅ same function instance unless its dependencies change
const handleClick = useCallback(() => {
console.log("clicked");
}, []);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Add</button>
<Child onClick={handleClick} />
</div>
);
}

Read the useCallback call carefully.

  • The first argument is the function you want to keep.
  • The second argument is the dependency array, the values the function depends on.
  • React saves the function and hands back the same one on the next render, as long as the dependencies are unchanged.
  • Now Child gets the exact same onClick each time, so React.memo finally works and the child stops re-rendering.

Output

Child rendered
(user clicks Add - Child does NOT re-render)
(user clicks Add - Child does NOT re-render)

🔗 useCallback is just useMemo for functions

These two hooks are the same idea wearing different clothes. useMemo remembers a value. useCallback remembers a function.

  • useMemo(() => fn, deps) would return the function fn itself.
  • useCallback(fn, deps) is just a shortcut that does exactly that.
  • So useCallback(fn, deps) and useMemo(() => fn, deps) give the same result.
  • One caches what a function returns, the other caches the function itself.

So if you understand useMemo, you already understand useCallback.

Same dependency rules

Just like useMemo, anything your function reads, props or state, should go in the dependency array. Leave one out, and the saved function may use a stale value from an old render.

🌍 Where useCallback actually helps

useCallback only pays off in specific spots, so using it everywhere just adds noise. Here’s where it earns its place.

  • You pass a function to a child wrapped in React.memo, and you want that memo to work.
  • A function is listed in a useEffect dependency array, and you don’t want the effect to fire every render.
  • A function goes to many children, or deep down a tree, where a new identity would cause a cascade of re-renders.
  • A custom hook returns a function that callers will put in their own dependency arrays.

So the common thread is: something is comparing your function. If nothing compares it, you don’t need useCallback.

⚠️ Common Mistakes

The number one mistake is wrapping every function out of habit. Without a memoized child or an effect dependency, it does nothing useful.

// ❌ pointless - nothing compares this function, the child is not memoized
const handleClick = useCallback(() => doStuff(), []);
return <button onClick={handleClick}>Go</button>;
// ✅ a plain function is fine here
function handleClick() {
doStuff();
}
return <button onClick={handleClick}>Go</button>;

Keep these in mind.

  • Don’t use it when nothing compares the function. A plain function is simpler and just as fast.
  • Don’t forget dependencies. A stale closure means your function uses old state or props.
  • Don’t confuse it with useMemo. useCallback(fn, deps) returns the function; useMemo(fn, deps) returns what fn returns.

✅ Best Practices

A few rules keep useCallback useful and not just clutter.

  • Reach for it when you pass functions to React.memo children or list them in effect dependencies.
  • Always include every value the function reads in the dependency array.
  • Pair it with React.memo on the child. useCallback alone, without a memoized child, usually changes nothing.
  • If a function isn’t passed down or watched anywhere, leave it as a plain function.

It works as a team

useCallback rarely helps by itself. Its usual partner is React.memo on the child that receives the function. Use the two together, or the stable function has nothing to gain you.

🧩 What You’ve Learned

  • ✅ Functions inside a component are recreated on every render, a new identity each time
  • ✅ A new function prop makes React.memo children re-render and effect dependencies fire needlessly
  • useCallback(fn, deps) returns the same function until something in deps changes
  • ✅ It is just useMemo for functions: useCallback(fn, deps) equals useMemo(() => fn, deps)
  • ✅ List every value the function reads in the dependency array to avoid stale values
  • ✅ It mainly helps with memoized children and effect dependencies, not by itself

Check Your Knowledge

Test what you learned. Pick an answer for each question, then click Check.

  1. 1

    What does useCallback return?

    Why: useCallback caches the function itself, handing back the same instance until a dependency changes. This keeps its identity stable across renders.

  2. 2

    How is useCallback related to useMemo?

    Why: useCallback is useMemo specialized for functions. useCallback(fn, deps) gives the same result as useMemo(() => fn, deps).

  3. 3

    When does useCallback actually help?

    Why: useCallback pays off when something compares the function: a memoized child, or an effect dependency. If nothing compares it, a plain function is simpler.

  4. 4

    Why does useCallback usually need React.memo on the child to be useful?

    Why: A stable function only matters if the child skips re-rendering when its props are unchanged. That skipping is what React.memo provides, so the two work as a pair.

🚀 What’s Next?

Now you can keep both values and functions stable across renders. Next we move to a different problem: sharing data across many components without passing props through every level in between. That’s what the next hook is built for.

React useContext Hook

Share & Connect