Mastering useCallback and useMemo for React Performance
Stop unnecessary re-renders by mastering useCallback and useMemo. Learn to stabilize references and optimize dependency arrays to keep your React app fast.
Previously in this course, we explored Strategic use of React.memo to prevent child components from re-rendering when their props haven't changed. However, memoization is only half the battle. If your component passes functions or objects as props, those props might be re-created on every render, breaking the shallow comparison that React.memo relies on.
In this lesson, we master useCallback and useMemo to enforce Referential Equality, ensuring our memoized components stay performant instead of constantly re-rendering.
The Core Problem: Referential Equality
In JavaScript, objects and functions are compared by reference, not by value. When you define a function inside a component body, it is a new function instance every time the component renders.
JAVASCRIPT// This function is created anew on every render const handleClick = () => console.log(CE9178">'Clicked!');
Even if handleClick has the same logic as the previous render, prevProps.handleClick === nextProps.handleClick will be false. If you pass this function to a child wrapped in React.memo, that child will re-render every single time the parent does. This is the "unstable reference" trap.
Comparing the Hooks
| Hook | Purpose | Return Value |
|---|---|---|
useMemo | Cache the result of a calculation | The calculated value |
useCallback | Cache the function instance itself | The memoized function |
Stabilizing References with useCallback
Use useCallback to prevent a function from being recreated unless its dependencies change. This is critical when passing callbacks to memoized components or triggering effects.
Worked Example: Stabilizing a List Item
In our project, we have a ProductList component. We want to avoid re-rendering every ProductItem when the parent state updates, unless the specific item's data changes.
JSXimport React, { useCallback } from CE9178">'react'; const ProductList = ({ items }) => { // Without useCallback, this function is new every render. // Any child using this prop will re-render needlessly. const handleSelect = useCallback((id) => { console.log(CE9178">'Selecting:', id); }, []); // Dependency array: empty because it doesn't rely on state return ( <ul> {items.map(item => ( <MemoizedProductItem key={item.id} item={item} onSelect={handleSelect} /> ))} </ul> ); };
Optimizing Expensive Calculations with useMemo
useMemo is for compute-heavy operations. If you're filtering a massive array or transforming data, you don't want to repeat that work on every render.
JSXconst expensiveData = useMemo(() => { return heavyCalculation(items); }, [items]); // Only recalculate if CE9178">'items' changes
Managing Dependency Arrays
The primary pitfall with these hooks is the Dependency Array. If you omit a dependency, your hook will use stale data. If you include too many, the memoization becomes useless because the hook recalculates constantly.
The "Stale Closure" Trap
A common mistake is referencing a state variable inside a useCallback but forgetting to add it to the dependency array.
JSX// BAD: CE9178">'count' is used but not in dependencies const increment = useCallback(() => { setCount(count + 1); }, []); // Stale closure! CE9178">'count' will always be the initial value.
The Fix: Use the functional update form of setState to avoid the dependency entirely:
JSXconst increment = useCallback(() => { setCount(prev => prev + 1); }, []); // Perfectly stable
Hands-on Exercise
- Open your project's main dashboard component.
- Find a component that receives a callback prop.
- Use the Profiling with React DevTools techniques we learned earlier to confirm that the child component re-renders even when its data hasn't changed.
- Wrap the callback in
useCallbackand the data transformation logic inuseMemo. - Verify in the Profiler that the child no longer re-renders unnecessarily.
Common Pitfalls
- Over-memoization: Memoization has a cost (memory and comparison overhead). Don't wrap every function or object in these hooks. Only use them when you've identified a performance bottleneck or are passing props to a
React.memocomponent. - Ignoring the Dependency List: Use the
eslint-plugin-react-hooksruleexhaustive-deps. It will catch 99% of your dependency errors. - Referential Equality confusion: Remember that
useMemoreturns a value, whileuseCallbackreturns a function. You cannot useuseMemoto return a function unless it is a factory function returning a function.
Recap
useCallback and useMemo are your primary tools for stabilizing references in React. By ensuring that props remain referentially equal across renders, you allow React.memo to effectively bail out of rendering sub-trees, keeping your application snappy even as complexity grows. Remember: memoize only when you have a performance reason, and always keep your dependency arrays accurate.
Up next: We will look at State Colocation Strategies, where we move state closer to where it's actually used to further reduce the surface area of re-renders.
Work with me

Headless WordPress + Next.js Frontend Development
Keep WordPress for content, get a lightning-fast Next.js frontend. The best of both worlds โ familiar editing, modern speed.

React & Next.js Dashboard / Admin UI Development
A clean, data-rich dashboard UI in React or Next.js โ charts, tables, and real-time data that your users will actually enjoy using.