New developers often hear or read that useCallback
and useMemo
are tools to improve the performance
of a React application.
As a result, they start using them everywhere, hoping to optimize their app. But this usually leads to cluttered, overly verbose code, filled with complex dependency arrays—and ironically, it often hurts more than it helps.
In this post, we’ll explore what useCallback
and useMemo
actually do, when they’re useful, and—more importantly—when to avoid them.
Lets learn about what useCallback
and useMemo
do:-
useCallback:- "remembers" a function which you passed to it, so React doesn’t make a brand new version of that function every time your component updates.
const handleClick = useCallback(() => {
console.log("Button clicked");
}, []);
In above snippet, handleClick will remain the same across renders as long as the dependency array ([]) doesn't change. Without
useCallback
, a new function would be created on every render.
useMemo:- lets React remember (or "cache") the result of a calculation so it doesn’t redo it every time your component updates.
const expensiveValue = useMemo(() => {
return computeHeavyValue(input);
}, [input]);
Here,
computeHeavyValue(input)
runs only when input changes. If input is the same on the next render, React reuses the cached result instead of recalculating.
Doing these things(remebering of your values when props are changed) comes with a cost.
The Hidden costs of useCallback
and useMemo
The core idea behind using useCallback and useMemo is that memoization isn't free—it's a trade-off. You're spending extra CPU cycles and memory to cache a function or value, with the hope that it will save more expensive computations during future renders. But if not used wisely, the cost of memoizing can outweigh the benefits.
1. memory overhead:- This is the most direct cost. When you use useCallback
or useMemo
, React must store the memoized function or value in memory between renders.
2. CPU overhead:- Before React can decide whether to use the cached value or re-run the function React must iterate through the dependencies array and perform a shallow comparison of each item with the values from the previous render.
3. Readability and Maintenance:- Memoizing every function or value creates verbose code that is difficult to read and understand. These hooks force developers to manage dependency array carefully.Forgetting or adding a new dependecy can create a subtle bug.
const memoizedFn = useCallback(() => {
doSomething(a, b);
}, [a]); // Missing b in dependencies — can cause bugs
When not to optimize
Let’s look at a case where using useCallback
is unnecessary:
1.
const logEvent = useCallback(() => {
analytics.track('button_click');
}, []);
<button onClick={logEvent}>Click me</button>
In this example, the logEvent function is simple and is not passed to any
memoized child components
.
Therefore, using useCallback provides no real benefit and only adds unnecessary complexity.
2.
const UserInput = React.memo(({ onChange }) => {
const handleChange = useCallback(event => {
onChange(event.target.value);
}, [onChange]);
return <input onChange={handleChange} />;
});
In above example
handleChange
is always created based ononChange
, which is the only prop so when the parent re-renders and passes a newonChange
prop, both theReact.memo
anduseCallback
will trigger a re-render and recreatehandleChange
. IfonChange
hasn't changed,React.memo
will prevent re-render, sohandleChange
isn't recalculated either — even withoutuseCallback
. So hereuseCallback
is not used properly.
3.
const _debounce = (func, delay) => { /* returns debounced function */ };
const makeApiCall = (e) => { console.log("Making an API call"); };
const debounce = useCallback(_debounce(makeApiCall, 2000), []);
<input onChange={e => debounce(e.target.value)} />
Despite using
useCallback
, the function is redefined each time (because the function call is inlined), so all the memoization is pointless and adds CPU cost and memory use.
Now let’s explore at the cases where using useMemo
is unnecessary:
- Memoizing small calculation
const filteredTodos = useMemo(() => {
return todos.filter(todo => todo.completed);
}, [todos]);
Why it's overkill:- the
.filter
here is very fast, and inline use of it would be more readable as well.useMemo
adds memory overhead and dependency complexity here.
- Memoizing a JSX Tree
const todoRows = useMemo(() => {
return todos.map(todo => <ToDoRow key={todo.id} todo={todo} />);
}, [todos]);
useMemo
just stores a value — but React will still re-render each child component unless they're memoized withReact.memo
.
When to Optimize
useCallback, useMemo can be powerful tools—but only when used in the right situations.
- Optimizing Expensive Computation with useMemo-
const filteredUsers = useMemo(() => {
// Expensive filtering logic
return users.filter(user => user.name.toLowerCase().includes(searchTerm.toLowerCase()));
}, [users, searchTerm]);
In the example above,
useMemo
memoizes the filtered list of users, so the filtering function only runs again if either the users array or the searchTerm changes. This optimization can significantly improve performance by avoiding unnecessary recalculations on every render, keeping your UI responsive even with large datasets.
- Stable Callback for Child Components with useCallback-
const handleUserSelect = useCallback((userId) => {
// Logic for selecting a user
setSelectedUserId(userId);
}, [setSelectedUserId]);
In this example,
useCallback
memoizes thehandleUserSelect
function so that its reference only changes ifsetSelectedUserId
changes. This stability prevents child components relying on handleUserSelect from re-rendering unless truly necessary, improving rendering efficiency and overall app performance.
- useCallback and useMemo Together for Linked Operations-
const filteredUsers = useMemo(() => {
return users.filter(user => user.name.toLowerCase().includes(searchTerm.toLowerCase()));
}, [users, searchTerm]);
const handleUserSelect = useCallback((userId) => {
setSelectedUserId(userId);
}, [setSelectedUserId]);
return (
<UserList users={filteredUsers} onUserSelect={handleUserSelect} />
);
useMemo
memoizes the filtered list of users to avoid recalculating the filtered array on every render unless users or searchTerm changes.useCallback
memoizes thehandleUserSelect
function to keep its reference stable, preventing unnecessary re-renders in child components that receive it as a prop.
By combining these two hooks, you ensure that the child component <UserList>
receives both stable data and stable callbacks, which helps React skip unnecessary renders, improving performance and keeping your UI responsive.
Summary
useCallback
and useMemo
are powerful hooks that can boost React app performance but only when applied thoughtfully. Overusing them leads to complex, harder-to-maintain code and sometimes even worse performance.
Always ask yourself:
Is the function or value expensive to compute?
Is it passed to memoized child components that depend on stable references?
Will memoization reduce unnecessary renders or recalculations meaningfully?
If the answer is yes, useCallback
and useMemo
can be great allies. If not, keeping your code simple and clear often serves you better.
Remember, premature optimization
is the root of all evil—optimize only when you know the problem exists!
Top comments (0)