Should you add them to every render?
When it comes to React performance, two hooks jump to mind: useCallback and useMemo.
Many devs assume they belong everywhere. In reality, these hooks can sometimes do more harm than good.
Let’s clarify what they do, when to use them, and common misconceptions.
What is useCallback?
useCallback keeps a function’s reference stable across renders. Since functions are recreated on each render, child components may re-render unnecessarily.const handleClick = useCallback(() => {
console.log('Clicked!')
}, [])
Now the function reference stays the same, reducing needless child renders.When to use it
- You pass a function as a prop to a child.
- That child is optimized with
React.memo(). - Large lists or frequently re-rendering trees.
When to skip it
- The function is used only in the same component.
- Render frequency is low.
- There’s no measured performance issue.
What is useMemo?
useMemo caches a computed value until its dependencies change. It prevents re-running expensive calculations on every render.const sortedList = useMemo(() => {
return items.sort((a, b) => a.value - b.value)
}, [items])
As long as items is unchanged, the sort won’t rerun.When to use it
- Heavy computations (tables, filters).
- Derived state (totals, aggregates).
- When render includes costly calculations.
When to skip it
- Trivial math/logic.
- Cheap components.
- When object/array identity isn’t important.
useCallback vs useMemo
| Aspect | useCallback | useMemo |
|---|---|---|
| What it caches | Function reference | Computed value |
| Recomputes when | Dependencies change | Dependencies change |
| Purpose | Reference stability | Computation cache |
| Returns | Function | Value |
useCallback, values → useMemo.Real-world example
function ProductList({ products, onSelect }) {
const filtered = useMemo(() => products.filter(p => p.inStock), [products])
const handleClick = useCallback((id) => onSelect(id), [onSelect])
return (
<ul>
{filtered.map(p => (
<li key={p.id} onClick={() => handleClick(p.id)}>
{p.name}
</li>
))}
</ul>
)
}
useMemocaches the filtered list.useCallbackstabilizes the click handler reference.
Misuse example
const doubled = useMemo(() => value * 2, [value])
This is unnecessary—value * 2 is cheap. useMemo adds memory and GC overhead; it can backfire.💡 Remember: useMemo doesn’t always help; measure before adding.
With React.memo
React.memo prevents a child from re-rendering if its props are equal. But if function props change identity on every render, memo won’t help—this is where useCallback matters.const Child = React.memo(({ onClick }) => {
console.log('Child render!')
return <button onClick={onClick}>Click</button>
})
function Parent() {
const handleClick = useCallback(() => console.log('Clicked!'), [])
return <Child onClick={handleClick} />
}
Now Child won’t re-render on every Parent render.Deep computations with useMemo
useMemo is great for expensive math too:const fibonacci = useMemo(() => calcFibonacci(40), [])
But if dependencies change every render, the cache won’t help. 😊💡 Pro tip: No optimization without profiling. Add useMemo only where it moves the needle.
Measuring with DevTools
Use the React DevTools Profiler to measure render times. Only after identifying slow components should you applyuseMemo/useCallback.Steps
- Open Profiler.
- Interact (e.g., type in an input).
- Inspect render times.
- Add
memo/useCallbackand compare.
Summary table
| Goal | Hook | Watch out |
|---|---|---|
| Stabilize function prop identity | useCallback | Only if passed to children |
| Cache expensive derived values | useMemo | Only if it’s truly costly |
| Reduce child re-renders | React.memo | Function props must be stable |
Conclusion
useCallback and useMemo are micro-optimization tools, not cure-alls. Used correctly, they help; used blindly, they add noise.In short:
useCallback→ preserves function identity.useMemo→ caches computed results.React.memo→ skips re-renders when props don’t change.