Avoiding Common Pitfalls with useCallback and useMemo in React
Modern React development encourages performance optimization, and two hooks that often come up are useCallback and useMemo. While these hooks can be powerful, they are frequently overused, sometimes even harming performance instead of improving it.
This article explains when to use them, when to avoid them, and how to write more efficient React code without unnecessary complexity.
1. Understanding the Purpose of useCallback and useMemo
Before discussing overuse, it is important to understand what these hooks actually do.
useCallback
useCallback is used to memoize a function so that React does not recreate it on every render unless its dependencies change.
import { useCallback } from "react";
function Example( { value }) {
const handleClick = useCallback(() => {
console.log(value);
}, [value]);
return <button onClick={handleClick}>Click</button>;
}
In this example, the handleClick function is only recreated when value changes. On the surface, this seems like a good optimization.
However, what actually happens behind the scenes is that React stores the previous function reference and compares dependencies on every render. This means there is a cost associated with using useCallback. If the function is cheap to create, this cost may outweigh the benefit.
useMemo
Just like useCallback, useMemo is designed for optimization, but instead of memoizing functions, it memoizes computed values. It allows React to reuse the result of a calculation instead of recomputing it on every render.
import { useMemo } from "react";
function Example( { items }) {
const filteredItems = useMemo(() => {
return items.filter(item => item.active);
}, [items]);
return <div>{filteredItems.length}</div>;
}
Here, the filtering logic only runs when items changes. While this looks efficient, it is only beneficial if the filtering operation is expensive or the component re-renders frequently. Otherwise, the overhead of tracking dependencies and storing results can make things worse.
2. The Hidden Cost of Memoization
Memoization is often treated as a free optimization, but it introduces its own overhead. React needs to track dependencies, store previous values, and perform comparisons on every render.
const result = useMemo(() => computeExpensiveValue(data), [data]);
Although this avoids recomputing computeExpensiveValue, React still performs dependency checks and memory management. If computeExpensiveValue is actually fast, then the memoization layer adds unnecessary work.
This is why blindly adding useMemo or useCallback everywhere can degrade performance instead of improving it. The key is to understand that memoization should be used selectively, not by default.
3. Overusing useCallback for Simple Functions
One of the most common mistakes is wrapping every function in useCallback, even when there is no real benefit.
function Counter( { count }) {
const increment = useCallback(() => {
console.log("Increment clicked");
}, []);
return <button onClick={increment}>{count}</button>;
}
In this case, the increment function does not depend on any changing values and is not passed to a memoized child component. React can recreate this function extremely quickly, so memoizing it adds unnecessary complexity.
A simpler and more effective approach is to define the function normally.
function Counter( { count }) {
const increment = () => {
console.log("Increment clicked");
};
return <button onClick={increment}>{count}</button>;
}
This version is easier to read and avoids the overhead of dependency tracking. In most scenarios, this is sufficient.
4. Using useCallback Without React.memo
Another common misuse of useCallback is using it in isolation without pairing it with React.memo.
function Parent() {
const handleClick = useCallback(() => {
console.log("Clicked");
}, []);
return <Child onClick={handleClick} />;
}
At first glance, this looks optimized. However, if the Child component is not memoized, it will re-render every time the parent renders regardless of whether handleClick changes.
To make useCallback effective, the child component must be memoized.
const Child = React.memo(({ onClick }) => {
console.log("Child rendered");
return <button onClick={onClick}>Click</button>;
});
function Parent() {
const handleClick = useCallback(() => {
console.log("Clicked");
}, []);
return <Child onClick={handleClick} />;
}
Now, React can skip re-rendering Child if its props remain the same. This is one of the few scenarios where useCallback provides real value.
5. Overusing useMemo for Cheap Computations
Developers often use useMemo for very simple calculations that do not need optimization.
function User( { firstName, lastName }) {
const fullName = useMemo(() => {
return firstName + " " + lastName;
}, [firstName, lastName]);
return <div>{fullName}</div>;
}
This is unnecessary because string concatenation is extremely fast. The memoization overhead is more expensive than the computation itself. A better approach is to compute the value directly.
function User( { firstName, lastName }) {
const fullName = firstName + " " + lastName;
return <div>{fullName}</div>;
}
This keeps the code simple and avoids unnecessary optimization logic.
6. Misusing useMemo for Derived State
Another pattern is using useMemo for derived data that does not need memoization.
function List( { items }) {
const activeItems = useMemo(() => {
return items.filter(item => item.active);
}, [items]);
return (
<ul> {activeItems.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}
This approach is only beneficial if the items array is very large or the filtering operation is computationally expensive. In many cases, it is perfectly fine to compute this directly during rendering.
function List( { items }) {
const activeItems = items.filter(item => item.active);
return (
<ul>
{activeItems.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}
This version is simpler and avoids unnecessary memoization unless performance profiling proves otherwise.
7. When You Should Use useCallback
Despite the risks of overuse, useCallback does have valid use cases when applied correctly.
const Item = React.memo(({ onSelect }) => {
return <button onClick={onSelect}>Select</button>;
});
function List() {
const handleSelect = useCallback(() => {
console.log("Item selected");
}, []);
return <Item onSelect={handleSelect} />;
}
In this scenario, useCallback ensures that the function reference remains stable, allowing the memoized Item component to avoid unnecessary re-renders. The key takeaway is that useCallback is most useful when function identity affects rendering behavior.
8. When You Should Use useMemo
useMemo is most effective when dealing with expensive computations or large datasets.
function Dashboard( { data }) {
const sortedData = useMemo(() => {
return [...data].sort((a, b) => a.value - b.value);
}, [data]);
return <div>{sortedData.length}</div>;
}
Sorting large datasets can be expensive, so memoizing the result can significantly improve performance if the component re-renders frequently. However, this should only be done after identifying a real performance bottleneck.
9. Measure Before You Optimize
Optimization should always be driven by measurement, not assumptions. React provides tools to help identify performance issues.
console.time("filter");
const result = items.filter(item => item.active);
console.timeEnd("filter");
You can also use the React DevTools Profiler to analyze component re-renders and identify bottlenecks. Without measurement, adding memoization is just guesswork and often leads to unnecessary complexity.
Cleaner React Code Without Over-Memoization
One of the most important principles in React development is prioritizing readability and simplicity.
function Dashboard( { users }) {
const activeUsers = users.filter(user => user.active);
return (
<ul>
{activeUsers.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
This example avoids unnecessary hooks and keeps the logic straightforward. In most cases, this approach is not only easier to maintain but also performs well enough.
10. Conclusion
Overusing useCallback and useMemo is a common pitfall in React development. While these hooks are powerful, they are not meant to be used everywhere.
They introduce overhead, increase code complexity, and can even reduce performance when used incorrectly. The best approach is to start with simple, readable code and only introduce memoization when a real performance issue has been identified.
By focusing on clarity first and optimization second, you can build React applications that are both efficient and easy to maintain.
This article explored how to avoid overusing useCallback and useMemo in React.

