React’s useMemo and useCallback can help avoid some unnecessary work, but adding them everywhere can make code harder to read without making it faster. The useful distinction is simple: useMemo caches a calculated value; useCallback caches a function reference. This guide explains when that distinction matters, how dependencies and referential equality affect memoized components, and how to decide whether memoization is worth using.
Key Takeaways
- Use
useMemoto cache a calculation’s result between renders when its dependencies have not changed and the calculation is worth avoiding. - Use
useCallbackto keep a function reference stable when that identity matters, often because it is passed to a component wrapped inmemoor used as another Hook’s dependency. - Neither Hook prevents a component from rendering by itself.
memocan skip a child render when its props compare equal; stable props can help that comparison succeed. - Include every reactive value used by the calculation or callback in its dependency array. Memoization is a performance optimization, not a correctness guarantee.
- If there is no measured or clear performance problem, prefer the simpler code without memoization.
What useMemo and useCallback actually cache
A component body runs again when React renders that component. Without memoization, calculations and function expressions in the body are evaluated again on each render.
useMemo caches the value returned by a calculation:
const visibleItems = useMemo(() => filterItems(items, query), [items, query]);
React can reuse the previous result while items and query have not changed. The calculation runs again when a dependency changes.
useCallback caches a function reference:
const handleSelect = useCallback((id) => selectItem(id), [selectItem]);
It is useful when the identity of that function matters to something else. It does not cache the result of calling the function. Conceptually, useCallback(fn, dependencies) is similar to useMemo(() => fn, dependencies).
Both Hooks compare each dependency with its previous value using Object.is. The dependency list must have a constant number of items and be written inline. Include every reactive value used inside the calculation or function; the React Hooks lint rules can help find omissions.
Why referential equality matters
Two objects or functions can do the same thing and still have different identities. For example, a new object literal or function expression created during a render is a new reference. In JavaScript, {} is not Object.is-equal to another {}, even if their properties match.
That matters when a component uses memo. By default, memo compares each prop with its previous value using Object.is. If a parent passes a newly created array, object, or callback on every render, that prop is different by reference. The child may render again even if the underlying information appears unchanged.
A stable reference can help memo skip a child render, but it does not guarantee that the child will never render. The child can still render for its own state or context changes, and React may render for other reasons. Memoization is an optimization, not a way to make rendering behavior correct.
A practical example: searchable task list
Here, filtering the tasks is memoized, and the toggle callback is stable. TaskList is wrapped in memo, so unchanged props can allow React to skip rendering it when TaskBoard renders for another reason.
import { memo, useCallback, useMemo, useState } from 'react';
const TaskList = memo(function TaskList({ tasks, onToggle }) {
return (
<ul>
{tasks.map((task) => (
<li key={task.id}>
<label>
<input
type="checkbox"
checked={task.done}
onChange={() => onToggle(task.id)}
/>
{task.title}
</label>
</li>
))}
</ul>
);
});
export default function TaskBoard() {
const [query, setQuery] = useState('');
const [tasks, setTasks] = useState([
{ id: 1, title: 'Review pull request', done: false },
{ id: 2, title: 'Update release notes', done: true },
{ id: 3, title: 'Reply to support ticket', done: false },
]);
const visibleTasks = useMemo(() => {
const normalizedQuery = query.trim().toLowerCase();
return tasks.filter((task) =>
task.title.toLowerCase().includes(normalizedQuery)
);
}, [tasks, query]);
const handleToggle = useCallback((id) => {
setTasks((currentTasks) =>
currentTasks.map((task) =>
task.id === id ? { ...task, done: !task.done } : task
)
);
}, []);
return (
<section>
<label>
Search tasks
<input
value={query}
onChange={(event) => setQuery(event.target.value)}
/>
</label>
<TaskList tasks={visibleTasks} onToggle={handleToggle} />
</section>
);
}
The functional state update lets handleToggle use the latest state without reading tasks from its closure. That is why an empty dependency array is appropriate for this particular callback. If the callback read a changing prop or state value directly, that value would generally need to be included in the dependencies.
When TaskBoard renders for a reason that changes neither tasks nor query, visibleTasks can keep its previous array reference. handleToggle also keeps its reference. Those stable props give the memoized TaskList an opportunity to skip a render. Changing the query or toggling a task changes the relevant data, so a render is expected.
When to reach for each Hook
Use useMemo for a worthwhile calculation or a reference-sensitive value
Consider it when a calculation is measurably expensive for the data your app handles, or when returning a stable value lets a memoized child avoid unnecessary work. Filtering a tiny list may be cheap enough that caching it brings no noticeable benefit; a costly transformation over a large collection is a stronger candidate.
The calculation should be pure: it should return a value rather than cause side effects. Don’t use useMemo to make an effect run correctly or to preserve state. If the result must be stored as state, manage it as state; if it is derived from current props or state, calculate it during rendering and memoize only if there is a performance reason.
Use useCallback when function identity is important
A callback is a useful candidate when it is passed to a child wrapped in memo, or when it is a dependency of another Hook and a changing reference would cause unnecessary work. If neither applies, defining the function in the component is usually clearer.
Before adding useCallback to a child prop, check the whole path. If an ancestor passes a new callback or object into the component on every render, that changing prop may still trigger the component to render. Stabilizing one callback cannot make all props stable.
The cost of unnecessary memoization
Memoization is not free. It adds Hook bookkeeping, dependency comparisons, and cached references, and it can make dependency relationships more difficult to understand. It can also hide stale-value bugs if dependencies are missing. For a cheap calculation or a function whose identity is irrelevant, the simpler version is often the better choice.
If your project uses React Compiler, check the guidance for your compiler setup before adding manual memoization; the compiler may handle some memoization automatically. Don’t assume it is enabled just because the app uses React.
Common Mistakes
- Using
useMemooruseCallbackeverywhere: A stable reference has little value if nothing depends on its stability. - Treating a memoized component as a render guarantee:
memois an optimization. It does not prevent renders caused by the component’s own state, context, or other changes. - Leaving values out of dependencies: This can make a callback or calculation use outdated values. Fix the data flow or include the dependencies rather than suppressing the warning without understanding it.
- Depending on an object created during render: A freshly created object is a new dependency on every render, which can defeat memoization. Move its creation into the memoized calculation when appropriate, or otherwise make the dependency stable for a clear reason.
- Memoizing for correctness: Code should produce the correct result whether React reuses a cached value or recalculates it. Do not rely on a cache as persistent storage.
- Expecting
useCallbackto stop function creation: The function expression is still evaluated during rendering; the Hook is useful because React can return a previously cached function reference when dependencies match.
Best Practices
- Start with straightforward rendering code. Add memoization when profiling or a clear interaction pattern shows avoidable work.
- If a child needs to skip renders, check whether it is wrapped in
memoand whether all relevant props are stable—not just the callback. - Keep Hook calculations pure and dependency lists complete. Treat lint warnings about dependencies as useful feedback.
- Prefer functional state updates when they naturally avoid capturing state in a callback, as in the task example.
- Re-check the result after optimizing. If the code becomes harder to maintain without a visible benefit, remove the memoization.
Conclusion
Use useMemo to cache a value and useCallback to cache a function reference—but only when the saved work or stable identity matters. Understand the dependency list, consider how props reach memoized children, and keep correctness independent of the cache. When in doubt, begin with simpler code and measure before optimizing.
