React gives you a whole toolbox of hooks — some you use daily (useState), some you only dust off when things start feeling… slow.
useCallback is one of those “performance hooks” that often gets thrown around in conversations about avoiding unnecessary work.
The problem?
A lot of devs either overuse it (wrapping every function “just in case”) or misuse it (expecting it to magically speed things up).
In reality, useCallback is a very specific tool with a very specific job — and once you get that, you can use it with confidence instead of guesswork.
This guide is your roadmap — starting from the mental model, moving through common patterns, and ending with a simple checklist you can use to decide if useCallback is worth it in any given situation.
Table of Contents
- Why
useCallbackExists - Building the Mental Model
- The API & First Examples
- When
useCallbackHelps vs. When It’s Useless
Why useCallback Exists
Imagine this:
You’ve built a super-optimized React app.
You’ve even wrapped some of your components in React.memo to prevent unnecessary re-renders.
But then… something strange happens.
Even though the props look the same, your “memoized” child component keeps re-rendering every time the parent updates. 😤
The culprit?
Inline functions.
In JavaScript, functions are objects.
And just like {} creates a new object every time you run it,
() => {} creates a new function every time your component renders.
That means:
<MyChild onClick={() => console.log('clicked')} />
…creates a fresh onClick function object every render.
From React’s point of view, the onClick prop changed —
different reference → re-render triggered.
Where useCallback comes in
useCallback is basically useMemo for functions.
It says:
“Hey React, give me the same function reference unless one of these dependencies changes.”
So instead of creating a new function each render:
const handleClick = useCallback(() => {
console.log('clicked');
}, []);
React will hand you back the same exact function object every time — until something in [] changes.
Big picture difference from useMemo
-
useMemocaches a value (result of a calculation). -
useCallbackcaches a function (so the reference stays stable).
The goal isn’t to make the function itself faster —
it’s to make sure React doesn’t think “new function” and cause unnecessary updates.
Building the Mental Model
Here’s the thing:
Every time your React component runs (renders), it’s like React is calling your component function from scratch.
That means everything inside gets recreated — variables, objects, functions — unless you explicitly tell React to keep something the same.
Functions are objects too
In JavaScript, a function isn’t some magical, immutable entity — it’s just another object type.
So when you do:
function MyComponent() {
const greet = () => console.log('Hi!');
// ...
}
Every render:
- React calls
MyComponent()again. - A new
greetfunction is created in memory. - Even if it looks identical, its reference is different.
React’s reference equality check
When React decides whether a component’s props have changed, it uses shallow comparison — it checks if the reference is the same, not if the content “looks” the same.
That means:
() => console.log('Hi!') !== () => console.log('Hi!')
They’re two different function objects in memory.
Why this matters for props
If you pass a new function to a child on every render:
<MyChild onClick={() => console.log('clicked')} />
React sees:
-
Old render:
onClick→ Function A -
New render:
onClick→ Function B (new reference)
Even though Function A and Function B do the exact same thing, React thinks:
“Prop changed — better re-render the child.”
Where useCallback changes the game
When you wrap your function in useCallback:
const handleClick = useCallback(() => {
console.log('clicked');
}, []);
React stores the function once, and hands you back the same reference each render — until a dependency changes.
Now, the child sees:
- Render 1:
onClick→ Function A - Render 2:
onClick→ Function A (same reference) ✅
Result? No unnecessary re-render.
Analogy time:
Think of useCallback like giving React a permanent phone number for your function.
Without it, you’d be giving out a new number every time you spoke — and your friends (child components) would think they have to reintroduce themselves.
The API & First Examples
The syntax
useCallback looks almost exactly like useMemo:
const memoizedFn = useCallback(
(/* arguments */) => {
// function body
},
[/* dependencies */]
);
What happens here?
- On the first render, React stores your function.
-
On later renders, React checks the dependency array:
- If nothing in the array changed → return the same function reference as last time.
- If something did change → create and store a new function.
Example 1 — Preventing unnecessary child re-renders
Let’s start simple. We have a memoized child component:
const Child = React.memo(function Child({ onClick }) {
console.log('Child rendered');
return <button onClick={onClick}>Click Me</button>;
});
And a parent:
function Parent() {
const [count, setCount] = React.useState(0);
const handleClick = useCallback(() => {
console.log('Button clicked');
}, []);
return (
<>
<Child onClick={handleClick} />
<button onClick={() => setCount(count + 1)}>Increment</button>
<p>Count: {count}</p>
</>
);
}
Why this works:
-
Childis wrapped inReact.memo, so it skips re-rendering if its props haven’t changed. - Without
useCallback, a newhandleClickreference would be created every timeParentre-rendered, causingChildto render again unnecessarily. - With
useCallback,handleClickstays the same between renders, soChildonly renders once.
Example 2 — Stable callbacks for complex components
Some components (like charts, maps, or custom widgets) attach event listeners when they mount — and they might remove & re-add those listeners every time the callback reference changes.
Example:
function ChartWrapper({ data }) {
const handlePointClick = useCallback((point) => {
console.log('Point clicked:', point);
}, []);
return <MyBigChart data={data} onPointClick={handlePointClick} />;
}
Without useCallback, MyBigChart might think:
“Oh, new onPointClick function? Better re-bind all my event listeners.”
With useCallback, the reference stays stable, so setup work only happens once.
Dependency arrays matter
If your function depends on values from the component scope, you must list them in the dependency array:
const handlePointClick = useCallback((point) => {
console.log(point, filterValue); // filterValue is from scope
}, [filterValue]);
If you leave filterValue out, you’ll get stale closures — your function will “freeze” with the old value.
When useCallback Helps vs. When It’s Useless
It looks like the issue arises from how the markdown is being formatted. In markdown, sections are typically delineated by headings like ## (for subheadings), but in your case, I think you're expecting those sections to be separate, each starting with a proper heading.
When useCallback Helps
useCallback is not a magic performance booster for every function. It shines in specific situations:
1. Preventing unnecessary re-renders in memoized children
If you’re passing a function as a prop to a child wrapped in React.memo, useCallback ensures the function reference stays stable between renders, preventing unnecessary re-renders.
const handleClick = () => {
console.log("Button clicked!");
};
const MemoizedChild = React.memo(({ onClick }) => {
console.log("Child re-rendered");
return <button onClick={onClick}>Click Me</button>;
});
const Parent = () => {
return <MemoizedChild onClick={useCallback(handleClick, [])} />;
};
In this case, handleClick will not change between renders, and the MemoizedChild will not re-render unless its other props change.
2. Keeping dependencies stable for useEffect or useMemo
If you pass a function into a useEffect or useMemo dependency array, React will re-run the effect or memoization if the function reference changes. useCallback ensures that the function remains stable and the effect is only triggered when necessary.
const fetchData = useCallback(() => {
fetch("/api/data")
.then((response) => response.json())
.then((data) => setData(data));
}, []); // Stable dependency
useEffect(() => {
fetchData();
}, [fetchData]); // Will not run unless fetchData changes
Without useCallback, the fetchData function would be considered a new reference on every render, causing the effect to re-run unnecessarily.
3. Avoiding expensive teardown/setup in third-party components
Some UI components (like charts, maps, or editors) may attach event listeners when mounted and tear them down when the handler changes. By using useCallback, you avoid reattaching event listeners unnecessarily.
const handleResize = useCallback(() => {
console.log("Window resized");
}, []);
useEffect(() => {
window.addEventListener("resize", handleResize);
return () => {
window.removeEventListener("resize", handleResize);
};
}, [handleResize]); // Only re-add listener when handleResize changes
Here, handleResize remains stable, preventing unnecessary listener attachment and detachment on each render.
4. Functions in deeply nested prop chains
When you pass callbacks several layers deep, you don’t want each layer re-rendering just because the top component got a new function reference.
const handleClick = useCallback(() => {
console.log("Click event in deeply nested component");
}, []);
const Child = ({ onClick }) => {
return <GrandChild onClick={onClick} />;
};
const GrandChild = ({ onClick }) => {
return <button onClick={onClick}>Click Me</button>;
};
const Parent = () => {
return <Child onClick={handleClick} />;
};
In this example, useCallback ensures that handleClick doesn’t change unnecessarily, avoiding re-renders of Child and GrandChild.
When useCallback is Useless (or Harmful)
useCallback is not free — it adds a bit of overhead for React to keep track of your memoized function. If you use it everywhere, you may actually slow things down.
1. The child isn’t memoized
If the child component re-renders every time anyway, then useCallback won’t make a difference. There’s no memoization, so the function reference stability doesn’t matter.
const handleClick = () => {
console.log("Clicked!");
};
const Child = ({ onClick }) => {
console.log("Child re-rendered");
return <button onClick={onClick}>Click Me</button>;
};
const Parent = () => {
return <Child onClick={useCallback(handleClick, [])} />;
};
In this case, Child is not memoized, so wrapping handleClick with useCallback does not prevent the re-render of Child.
2. The function is cheap and the component renders rarely
If the function body is tiny (like setting state) and the component doesn’t render often, there’s no measurable performance gain.
const handleClick = () => setCount((prevCount) => prevCount + 1);
const Parent = () => {
return <button onClick={handleClick}>Increment</button>;
};
Since the function is small and the component is unlikely to re-render often, using useCallback here would add unnecessary overhead.
3. Premature optimization
If you haven’t measured the problem, you might be solving an issue that doesn’t exist. Use React DevTools Profiler to see if function props are really causing re-renders.
const handleClick = () => {
console.log("Button clicked");
};
const Parent = () => {
return <MemoizedChild onClick={handleClick} />;
};
In this case, if there’s no performance issue (e.g., the MemoizedChild doesn't re-render unnecessarily), using useCallback might be premature optimization.
Rule of Thumb:
Reach for useCallback when:
- You have a memoized child
- The only prop that changes is a function
- That change triggers a useless re-render
Otherwise, keep it simple.
Real-World Patterns
Once you understand what useCallback does, the fun part is spotting where it naturally solves problems in real projects.
Here are some patterns you’ll see all the time.
1. Event handler stability for memoized children
The problem: You have a React.memo child that takes an event handler prop. Without useCallback, it re-renders every time the parent updates.
Example:
const ListItem = React.memo(function ListItem({ onSelect, label }) {
console.log('Rendered:', label);
return <li onClick={onSelect}>{label}</li>;
});
function List({ items }) {
const handleSelect = useCallback((item) => {
console.log('Selected:', item);
}, []);
return (
<ul>
{items.map((item) => (
<ListItem
key={item}
label={item}
onSelect={() => handleSelect(item)}
/>
))}
</ul>
);
}
Now, only the list items that actually need to re-render will do so.
2. Stable callbacks for custom hooks
Some custom hooks accept callbacks, and those callbacks might go into a dependency array internally. If you don’t use useCallback, you can cause the hook’s internal effects to run on every render.
Example:
function useWindowResize(callback) {
React.useEffect(() => {
window.addEventListener('resize', callback);
return () => window.removeEventListener('resize', callback);
}, [callback]);
}
function App() {
const onResize = useCallback(() => {
console.log('Resized!');
}, []);
useWindowResize(onResize);
return <div>Resize me!</div>;
}
Without useCallback, the listener would be re-added on every render.
3. Debouncing or throttling without re-creating
If you wrap a function with debounce or throttle, you don’t want that wrapped function to be recreated every render — it would reset its internal timer.
Example:
import { debounce } from 'lodash';
function SearchInput() {
const [query, setQuery] = React.useState('');
const sendQuery = useCallback(
debounce((q) => console.log('Searching for:', q), 300),
[]
);
return (
<input
value={query}
onChange={(e) => {
setQuery(e.target.value);
sendQuery(e.target.value);
}}
/>
);
}
Here, useCallback ensures the debounced function lives across renders.
4. Avoiding infinite effect loops
A common beginner trap:
useEffect(() => {
doSomething();
}, [someFunction]);
If someFunction is created inline and not memoized, this effect will run on every render.
useCallback makes someFunction stable so the effect runs only when it should.
Pattern takeaway:
useCallback is like an insurance policy for function references in situations where stability matters — memoized children, effect dependencies, event bindings, and wrapped utilities.
Measuring First
Before you start sprinkling useCallback everywhere like seasoning, you need to know:
“Is this function reference problem actually slowing down my app?”
The truth is — many components re-render happily with zero performance issues. Adding useCallback unnecessarily can make your code more complex without any benefit.
Step 1 — Spotting the symptoms
Signs that useCallback might help:
- You have a memoized child (
React.memo) that still re-renders when the parent updates. - You see functions in dependency arrays causing effects to run too often.
- You have expensive setup/teardown in a child or third-party component when a callback changes.
Step 2 — Use the React DevTools Profiler
The React DevTools Profiler (in your browser’s dev tools) shows:
- How many times each component rendered.
- Why it rendered — including “props changed” as a reason.
If you see a child re-rendering due to a changed function prop, that’s a candidate for useCallback.
Step 3 — Test before & after
Wrap your function in useCallback and re-run the profiler.
- Did the extra re-renders disappear? ✅ Great — you found a win.
- No difference? ❌ Revert — you don’t need it here.
Step 4 — Keep a mental checklist
Before using useCallback, ask:
- Is there a measurable problem? (Profiler says yes)
- Is a function prop causing it?
- Will keeping the reference stable fix it?
If any answer is “no,” you can probably skip it.
Bottom line:
Measure first. Optimize second. Don’t let useCallback become a knee-jerk habit — use it as a targeted fix.
Pitfalls & Mistakes
Like most hooks, useCallback is powerful — but it’s easy to misuse it in ways that either do nothing or actively cause problems.
Let’s look at the most common traps.
1. Missing dependencies → stale closures
If your callback uses values from the component scope, you must include them in the dependency array.
Bad:
const handleClick = useCallback(() => {
console.log(count); // count is from scope
}, []); // ❌ Missing dependency
Here, handleClick will “freeze” with the count value from the first render.
When count changes, it still logs the old number.
Good:
const handleClick = useCallback(() => {
console.log(count);
}, [count]); // ✅ Include it
2. Overusing without benefit
Some developers wrap every function in useCallback “just in case.”
This adds unnecessary complexity, makes code harder to read, and slightly increases render overhead.
If there’s no measurable problem, skip it.
3. Using useCallback for side effects
useCallback is not for running code in response to changes — it’s for returning a stable function reference.
If you want to run code when something changes, use useEffect.
4. Forgetting that it doesn’t memoize the result
useCallback doesn’t store what the function returns — it stores the function itself.
If you want to memoize a computed value, use useMemo.
5. Thinking it always improves performance
In some cases, useCallback can actually make your app slower, because:
- React has to store and compare the memoized function.
- You may still be re-rendering for other reasons.
- The function itself is cheap to recreate.
Rule:
Only reach for useCallback when:
- The function is passed to a memoized child or
- The function is in a dependency array and
- Stabilizing the reference will avoid unnecessary work.
Wrap-up & Mental Checklist
We’ve gone from “useCallback is just useMemo for functions” to knowing exactly when and why to use it — and when it’s just clutter.
Your Quick useCallback Checklist
Before adding it, ask yourself:
Is there a measurable performance problem?
Check with the React DevTools Profiler first.Is a function reference causing it?
For example:
- A memoized child re-rendering because of a changing callback.
- An effect running too often because the function in its dependency array keeps changing.
Will stabilizing the function reference fix it?
If the issue is caused by something else,useCallbackwon’t help.Are your dependencies correct?
Missing dependencies = stale closures.
Key Takeaways
- What it does: Caches a function reference until dependencies change.
- Why it matters: Keeps props/effect dependencies stable, avoiding useless re-renders or effect runs.
- When it’s useful: Memoized children, dependency arrays, event listener setup.
- When to skip: Non-memoized children, cheap functions, rare renders.
Analogy to remember:
useCallback is like giving React your function’s phone number —
if the number stays the same, React won’t bother calling back unnecessarily.
Change the number only when you actually move house (dependencies change).
Up Next:
The Definitive React 19 useId Guide — Patterns, Pitfalls, and Pro Tips →
Follow me on DEV for future posts in this deep-dive series.
https://dev.to/a1guy
If it helped, leave a reaction (heart / bookmark) — it keeps me motivated to create more content
Want video demos? Subscribe on YouTube: @LearnAwesome
Top comments (0)