useCallback and useMemo are two of the most misunderstood React hooks. Often used reflexively "to optimize", they can actually hurt performance if applied everywhere without thinking.
In this article, I cut through the myths and give you clear rules for when to actually reach for them.
The core problem: re-renders in React
First, you need to understand how React decides to re-render a component. By default, a component re-renders every time its parent re-renders, regardless of whether its own props changed.
function Parent() {
const [count, setCount] = useState(0);
// This function is recreated on every render
const handleClick = () => console.log('clicked');
return <Child onClick={handleClick} />;
}Every time count changes, Parent re-renders, handleClick gets a new reference, and Child receives a new function prop → Child re-renders too, even though nothing relevant to it changed.
useCallback: memoize a function
useCallback returns a memoized version of a function. The function is only recreated when its dependencies change.
const handleClick = useCallback(() => {
console.log('clicked', id);
}, [id]); // Only recreated when `id` changesWhen useCallback is useful
Case 1: Prop passed to a memoized component with React.memo
const Child = React.memo(({ onClick }: { onClick: () => void }) => {
return <button onClick={onClick}>Click me</button>;
});
function Parent() {
const [count, setCount] = useState(0);
// Without useCallback: Child re-renders every time (new reference each time)
// With useCallback: Child only re-renders if id changed
const handleClick = useCallback(() => {
doSomething();
}, []); // Stable dependencies
return <Child onClick={handleClick} />;
}Case 2: Dependency of a useEffect
// Problem: fetchData is recreated on every render → useEffect loops infinitely
useEffect(() => {
fetchData(id);
}, [fetchData]); // ← fetchData changes on every render!
// Fix with useCallback
const fetchData = useCallback(async (id: string) => {
const data = await api.get(id);
setData(data);
}, []); // fetchData is now stable → useEffect won't loopWhen useCallback does nothing
// ❌ Useless: the component re-renders anyway
function SimpleComponent() {
const handleClick = useCallback(() => {
setCount(c => c + 1);
}, []);
// If this component doesn't use React.memo and no useEffect
// depends on handleClick, useCallback adds zero value
return <button onClick={handleClick}>+</button>;
}useMemo: memoize a computed value
useMemo memoizes the result of a computation. It only recomputes when its dependencies change.
const expensiveResult = useMemo(() => {
return data.filter(item => item.active).sort((a, b) => a.score - b.score);
}, [data]);When useMemo is useful
Case 1: Expensive computation on a large list
// ✅ Worth it if `items` has thousands of entries
const filteredItems = useMemo(() => {
return items
.filter(item => item.category === selectedCategory)
.sort((a, b) => b.date - a.date);
}, [items, selectedCategory]);Case 2: Object passed as prop to a memoized component
// ✅ Without useMemo, config is a new object on every render
const config = useMemo(() => ({
theme: 'dark',
locale: locale,
}), [locale]);
return <MemoizedChart config={config} />;When useMemo is counterproductive
// ❌ Useless: the computation is trivial
const doubled = useMemo(() => count * 2, [count]);
// Just write:
const doubled = count * 2;Memoization itself has a cost (storing the value, comparing dependencies). For simple computations, the overhead exceeds the benefit.
The golden rule
Here's how I decide whether to use these hooks:
-
Profile first with React DevTools. Never optimize blindly.
-
useCallback: only if the function is passed as a prop to aReact.memocomponent, or if it's in auseEffectdependency array. -
useMemo: only if the computation is measurably slow, or if the value is an object/array passed to a memoized component.
Conclusion
The temptation to wrap every function in useCallback and every computation in useMemo is real, it feels like writing "clean, optimized code". In practice, it's often the opposite.
React is already very fast. Trust the rendering engine, profile before optimizing, and only reach for these hooks when you have a measurable reason to do so.