React useCallback Hook
Table of Contents + −
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
handleClickis 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.memomakesChildskip re-rendering when its props are the same as before.- But
handleClickis recreated on everyParentrender, so it’s a different function each time. Childcompares the oldonClickto 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
Childgets the exact sameonClickeach time, soReact.memofinally 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 functionfnitself.useCallback(fn, deps)is just a shortcut that does exactly that.- So
useCallback(fn, deps)anduseMemo(() => 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
useEffectdependency 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 memoizedconst handleClick = useCallback(() => doStuff(), []);return <button onClick={handleClick}>Go</button>;
// ✅ a plain function is fine herefunction 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 whatfnreturns.
✅ Best Practices
A few rules keep useCallback useful and not just clutter.
- Reach for it when you pass functions to
React.memochildren or list them in effect dependencies. - Always include every value the function reads in the dependency array.
- Pair it with
React.memoon the child.useCallbackalone, 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.memochildren re-render and effect dependencies fire needlessly - ✅
useCallback(fn, deps)returns the same function until something indepschanges - ✅ It is just
useMemofor functions:useCallback(fn, deps)equalsuseMemo(() => 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
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
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
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
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.