useMemo vs useCallback: 5 Mistakes That Slow Down Your React App
Understanding the essence of memoization is the foundation for avoiding common pitfalls and building a cleaner, more maintainable codebase. This article takes a close look at how the two hooks work, how they differ, and, more importantly, the situations where you absolutely should not use them.

Trung Vũ Hoàng
Author
1. What Is Memoization in React?
One thing to understand first: every operation in React has a cost. Each time a component re-renders, all variables and functions inside it are re-created from scratch.
Memoization is a technique that caches computed results or function definitions. When the inputs (dependencies) haven't changed, React returns the cached data instead of recalculating—saving CPU resources.
However, caching requires memory and time to compare dependencies on each render. Premature, thoughtless optimization often does more harm than good.
2. useMemo: Memoizing Computation Results
useMemo is a hook that caches a function's return value—especially useful when that function performs expensive computations.
How It Works
On the initial render, the function passed to useMemo runs as usual. From the second render onward, React checks the dependency array:
If a dependency changes: The function runs again to produce a new result.
If dependencies are unchanged: React returns the previously cached result—no work is redone.
Real-World Example
You have a list of 5,000 products and need to filter by the user's search keyword. This is a heavy computation—but it only needs to run when the list or the query actually changes:
const filteredProducts = useMemo(
() => filterLogic(products, query),
[products, query]
);3. useCallback: Memoize Function Definitions
While useMemo caches a value, useCallback caches the function you pass in—it doesn't execute the function, it just remembers it.
Why Memoize a Function?
In JavaScript, functions are objects. Whenever a component re-renders, functions declared inside it receive a new reference in memory—even if their body doesn't change.
This becomes a problem when you pass that function to a child component wrapped in React.memo. Because the new function is not the same reference as the old one, the child component re-renders needlessly.
When Does useCallback Actually Help?
When passing a function as a prop to a child component wrapped with
React.memo.When the function is a dependency of another hook such as
useEffect.
4. Key Differences
Purpose
useMemo: Cache a value—number, string, array, object...
useCallback: Cache a function (function reference).
Return value
useMemo: The result after the callback has executed.
useCallback: The callback function itself, not executed.
Syntax relationship
In fact, useCallback(fn, deps) is equivalent to useMemo(() => fn, deps).
5. Five Common Memoization Mistakes
The habit of "wrapping everything in useMemo and useCallback" is a common anti-pattern. Here are practical reasons to stop:
Mistake 1: Dependency comparison costs outweigh the benefits
Comparing the dependency array on every render can cost more than recreating a simple function. Memoization only pays off when the work is truly expensive.
Mistake 2: Making code harder to read and maintain
Overusing these hooks turns a component into a maze of logic and dependencies. Teammates won't know where to start.
Mistake 3: Causing stale closures—the hardest bugs to track down
If you forget to include a variable in the dependency array, the function will read stale data—leading to silent logic bugs with no errors in the console.
Mistake 4: Applying it to primitive values
Types like number, string, and boolean are compared by value. Wrapping them in useMemo is completely redundant.
Mistake 5: Optimizing before measuring
Don't add memoization because you think something might be slow. Use React DevTools Profiler to identify real bottlenecks before intervening.
6. A Sound Optimization Strategy
A practical workflow I use in production projects:
Step 1: Write code naturally, without thinking about optimization.
Step 2: Use React DevTools Profiler to find components that re-render the most.
Step 3: Check whether those re-renders actually cause user-perceived lag. If not, ignore them.
Step 4: If optimization is needed, try refactoring the component (composition pattern) first.
Step 5: Only when necessary, use
useMemofor heavy computations anduseCallbackfor functions passed to children.
7. Summary
useMemo and useCallback are powerful when used in the right places, but unnecessary baggage when overused. Remember:
Use
useMemoto protect the results of expensive calculations.Use
useCallbackto keep function references stable when passing to children.Measure first, optimize later.
Have you ever added useMemo and found the app got slower? That's usually a sign of mistake #1 or #3 above!
Frequently Asked Questions
Bài viết liên quan

Zustand Async: 5 Effective Ways to Handle Async in React
Every real-world React app needs to communicate with async APIs. If handled poorly, it’s easy to run into issues like frozen UI, race conditions, memory leaks, or showing stale data. Zustand solves this with an extremely simple syntax—define async actions directly in the store, without complex middleware like Redux Thunk or Saga.

Advanced Promises: all, allSettled, race, any — When to Use Which?
If you only use sequential async/await, your app is wasting performance potential — each request has to wait for the previous one to finish before it can start. Promise’s static methods (all, allSettled, race, any) let you orchestrate multiple async tasks in parallel using the strategy that fits each problem.

Zustand Async: 5 Effective Ways to Handle Async Operations in React
Every real-world React app has to communicate with asynchronous APIs. If managed poorly, your app can run into issues like a frozen UI, race conditions, memory leaks, or showing stale data. Zustand solves this with an incredibly simple syntax — define async actions directly in the store, without complex middleware like Redux Thunk or Saga.