The Most Common Misconceptions
useCallback and useMemo are the most frequently misused hooks in React. They always cost something: memory, runtime for the comparison, cognitive complexity, and they only help in specific, measurable situations. Using them everywhere makes an application slower, not faster.
Table of Contents
- 1. How React renders, understanding the foundation
- 2. What useCallback actually does
- 3. What useMemo actually does
- 4. Misconception 1: useCallback prevents re-renders
- 5. Misconception 2: useMemo is always faster
- 6. React.memo: the missing piece of the puzzle
- 7. When useCallback and useMemo really help
- 8. Profiling with React DevTools
- 9. React Compiler: the end of manual memoization?
- 10. Summary
- 11. FAQ
1. How React renders, understanding the foundation
React renders a component when its state or props change. When a parent component renders, all child components re-render by default as well, regardless of whether their props changed. This is intentional and in most cases not a problem: React's reconciliation algorithm is very fast, and re-rendering a simple component costs only a few microseconds. The mistake lies in believing that re-renders are inherently bad. They are only expensive when they occur frequently and unnecessarily on slow components.
Understanding referential equality is the key to useCallback and useMemo. JavaScript compares objects and functions by reference, not by content. This means two functions with identical code are not equal (() => {} !== () => {}) because they are different objects on the heap. Every time a component renders, every function defined inside it is recreated as a new object with a new reference. If those functions are passed as props to child components, the child components see a new reference on every parent render and re-render as well.
This mechanism is the foundation on which useCallback and useMemo operate, and the reason both are so frequently misunderstood. They control referential equality to prevent unnecessary re-renders. But they only do so under one important condition: the child component must actually compare by referential equality, meaning it is either wrapped in React.memo or the reference is used as a dependency of another hook (useEffect, useQuery).
2. What useCallback actually does
useCallback(fn, deps) returns, on every render, the function that was created on the last render, as long as the entries in the dependency array are referentially equal. If a dependency changes, the function is recreated. This is essentially a caching mechanism for functions: the function is held in memory and its reference stays stable as long as the dependencies do not change.
The misconception is often the belief that useCallback makes the function itself faster. It does not. The function, when called, executes the exact same code as it would without useCallback. The only difference is the stability of the reference between renders. Holding it in memory also has a price: useCallback stores the function, stores the dependency array, and compares the array on every render. For a simple function with no performance problem, that is three operations for zero benefit.
// Demonstration: when useCallback helps vs. when it doesn't
import { useCallback, useState, memo } from "react";
// useCallback WITHOUT React.memo on the child: zero benefit
// Child re-renders on every parent render regardless
function ParentWrong() {
const [count, setCount] = useState(0);
// This stabilizes the reference, but HeavyChild is not memoized
const handleClick = useCallback(() => {
console.log("clicked");
}, []); // stable, but unused for optimization
return (
<div>
<button onClick={() => setCount((c) => c + 1)}>+{count}</button>
<HeavyChildNotMemoized onClick={handleClick} />
</div>
);
}
// useCallback WITH React.memo on the child: actual benefit
// Child only re-renders when handleClick reference changes
function ParentCorrect() {
const [count, setCount] = useState(0);
const [userId, setUserId] = useState(1);
// Stable reference: same function object between renders if userId didn't change
const fetchUser = useCallback(async () => {
const res = await fetch(`/api/users/${userId}`);
return res.json();
}, [userId]); // new reference only when userId changes
return (
<div>
<button onClick={() => setCount((c) => c + 1)}>Counter: {count}</button>
<MemoizedUserCard onFetch={fetchUser} />
</div>
);
}
// React.memo: only re-renders when props change by reference
const MemoizedUserCard = memo(function UserCard({ onFetch }: { onFetch: () => Promise<unknown> }) {
// This component renders only when onFetch reference changes
return <button onClick={onFetch}>Load user</button>;
});
3. What useMemo actually does
useMemo(fn, deps) executes the given function and stores its return value. On every render, the dependency array is compared. Only when a dependency changes is the function re-executed and the new value stored. Otherwise the stored value is returned. This is useful for computations that are expensive and depend on the same inputs: a filtered and sorted list of a thousand items, a formatted data transformation, a computed derivative of server data.
The second use case for useMemo is stabilizing object references that are passed as props. If a component passes a configuration object to a memoized child component, that object would be recreated on every render without useMemo, a new reference, causing the child component to re-render. With useMemo, the reference stays stable as long as the inputs do not change. As with useCallback: without React.memo on the child component, or without the reference being used as a dependency in a hook, this pattern brings no benefit.
4. Misconception 1: useCallback prevents re-renders
The most common misconception about useCallback: it prevents re-renders of the component in which it is called. That is not true. useCallback has no influence on whether the current component renders. It renders exactly when its state or its props change. useCallback only stabilizes the reference of a function so that child components might re-render less often.
Another common scenario: useCallback is applied to a function that is passed as an event handler on a native element (button, input). Native HTML elements do not use referential equality for event handlers, they are set anew on every render regardless. useCallback brings zero benefit here but adds the complexity of the dependency array and the memory cost of the cached function. This category accounts for the majority of useCallback calls in typical React codebases, and it is overengineering every single time.
5. Misconception 2: useMemo is always faster
The caching in useMemo has a measurable overhead: on every render, the dependency array must be compared. For every entry in the array, referential equality is checked. The result, either the cached computation or a new one, is stored. For simple computations like useMemo(() => a + b, [a, b]), the overhead of the memoization mechanism is greater than the overhead of the computation itself. const sum = a + b without useMemo is faster in this case.
The break-even point for useMemo lies with computations that are measurably expensive: filtering and sorting hundreds of objects, complex data transformations, regular expressions on long strings, geolocation calculations. These are the cases where a console.time() wrapper or the React Profiler shows that the computation actually costs milliseconds and runs frequently. Without that evidence, useMemo is premature optimization, with a negative sign attached.
// useMemo: when it helps vs. when it adds noise
import { useMemo, useState } from "react";
// useMemo on trivial computation: overhead > benefit
function PriceDisplay({ price, quantity }: { price: number; quantity: number }) {
// Addition is nanoseconds, memoization overhead is higher
const total = useMemo(() => price * quantity, [price, quantity]);
// Better: const total = price * quantity;
return <span>{total.toFixed(2)} EUR</span>;
}
// useMemo on genuinely expensive transformation
interface Order {
id: number;
status: "pending" | "shipped" | "delivered" | "cancelled";
total: number;
customer: string;
createdAt: string;
}
function OrderDashboard({ orders }: { orders: Order[] }) {
const [statusFilter, setStatusFilter] = useState<Order["status"] | "all">("all");
const [searchTerm, setSearchTerm] = useState("");
// Filtering 10,000+ orders: genuinely expensive, worth memoizing
const filteredOrders = useMemo(() => {
let result = orders;
if (statusFilter !== "all") {
result = result.filter((o) => o.status === statusFilter);
}
if (searchTerm.trim()) {
const lower = searchTerm.toLowerCase();
result = result.filter(
(o) =>
o.customer.toLowerCase().includes(lower) ||
String(o.id).includes(searchTerm)
);
}
return result;
}, [orders, statusFilter, searchTerm]); // recalculate only when inputs change
// Summary statistics, derived from filtered list, also expensive
const stats = useMemo(
() => ({
count: filteredOrders.length,
totalRevenue: filteredOrders.reduce((sum, o) => sum + o.total, 0),
avgOrderValue:
filteredOrders.length > 0
? filteredOrders.reduce((s, o) => s + o.total, 0) / filteredOrders.length
: 0,
}),
[filteredOrders]
);
return (
<div>
<p>{stats.count} orders, {stats.totalRevenue.toFixed(2)} EUR revenue</p>
</div>
);
}
6. React.memo: the missing piece of the puzzle
React.memo is the wrapper that makes useCallback and useMemo meaningful in the first place. Without React.memo on the child component, nothing changes in its rendering behavior, no matter how many references are stabilized in the parent element. React.memo(Component) creates a memoized version of the component that only re-renders when its props, compared by referential equality, have changed.
The complete pattern for optimized rendering is always a combination: React.memo on the child component, useCallback for function props in the parent, useMemo for object props in the parent. If one of these three is missing, the pattern breaks down. React.memo alone does not help if the parent component creates new function references on every render. useCallback alone does not help if the child component is not memoized. The most common mistake in practice: teams wrap functions with useCallback but forget React.memo, and then wonder why it has no effect.
7. When useCallback and useMemo really help
There are four legitimate use cases for useCallback in React. First, as a prop to a React.memo child component that should only re-render on reference change. Second, as a dependency in a useEffect, useQuery, or other hook where a new reference triggers an unwanted side effect. Third, for callbacks passed to external APIs such as event listeners, Intersection Observer, or websocket handlers. Fourth, for callbacks in custom hooks that are exposed externally and whose reference stability represents the API guarantee.
The legitimate use cases for useMemo: first, for measurably expensive computations on large datasets (profile before optimizing!). Second, to stabilize object references passed as props to React.memo components. Third, for computations whose result is used as a dependency in hooks. The rule of thumb: if the computation does not involve an array filter over more than 100 elements, no complex data enrichment, and no regular expressions, useMemo is probably overengineering. Measure first, optimize afterward.
8. Profiling with React DevTools
Before useCallback or useMemo is used, measurement must happen. The React DevTools Profiler tab is the primary tool for this. The workflow: open the Profiler, start recording, perform a typical user interaction, stop recording. The result shows every render as a bar: color (yellow = slow, blue = fast) and duration. Flamegraphs show which component in a render chain costs how much time. The "Ranked" view sorts components by render duration.
A concrete profiling workflow for re-render problems: enable "Highlight updates when components render" in the DevTools settings. Every component that renders unnecessarily briefly lights up. Then, in the Profiler, trace why it renders: "Rendered because" shows which props changed. If a prop is a function and changes on every parent render, that is a legitimate candidate for useCallback, but only if the child component is actually expensive to render. why-did-you-render as an npm package complements the Profiler with automatic console warnings for unnecessary re-renders.
9. React Compiler: the end of manual memoization?
The React Compiler (formerly React Forget) is a Babel/SWC plugin that automatically inserts useMemo and useCallback wherever it makes sense. The compiler plugin statically analyzes the component, detects pure computations and stable references, and memoizes them wherever it brings a benefit. The explicit goal is that developers no longer need to think manually about memoization. As of React 19, the compiler is production-ready and is used by Meta across all of their React applications.
The practical consequence for teams: with the React Compiler enabled, useCallback and useMemo are largely unnecessary for performance optimization. The compiler does it better and more consistently than manual decisions. The legitimate use cases remain: useCallback for API guarantees in custom hooks, useMemo for explicitly expensive computations as documentation of intent. For newly founded React 19 projects, the rule is: enable the React Compiler, do not add memoization hooks, measure, and only intervene manually when a problem is proven.
| Situation | useCallback worthwhile? | useMemo worthwhile? | Reasoning |
|---|---|---|---|
| Event handler on button | No | - | Native element, no reference comparison |
| Prop to React.memo child | Yes | Yes (objects) | Reference stability prevents re-render |
| Dependency in useEffect | Yes | Yes | Prevents infinite loop |
| a + b computation | - | No | Overhead > computation |
| Filter over 10,000 entries | - | Yes | Measurably expensive, Profiler confirms |
Mironsoft
React performance optimization and code quality
React app that feels slow?
We analyze React applications with the Profiler, identify real performance problems, and optimize with precision, without overengineering through excessive memoization.
Performance audit
Profiler analysis to identify expensive renders and unnecessary re-render cascades
Code review
Review of existing useCallback/useMemo usage for actual benefit and cleanup
React Compiler
Migration to React 19 with the React Compiler for automatic memoization without manual overhead
10. Summary
useCallback and useMemo are not general performance tools, they are specific solutions for specific problems. useCallback stabilizes function references so that memoized child components re-render less often, or so that hooks with function dependencies do not create infinite loops. useMemo stores expensive computation results or stabilizes object references. Both always cost memory and comparison overhead. Both only help when React.memo is present on the child component, or a hook that uses the reference as a dependency is present.
The correct order is always: measure, understand, optimize. The React DevTools Profiler shows which renders are actually expensive. why-did-you-render shows which re-renders are unnecessary. Only after this diagnosis is it clear whether useCallback, useMemo, React.memo, or a structural change is the right tool. With the React Compiler enabled, the compiler takes over most of these decisions, and manually added memoization often becomes duplicate work or even an obstacle for the compiler.
useCallback and useMemo, the essentials at a glance
useCallback helps when...
Function is passed as a prop to a React.memo child component or as a hook dependency. Not for event handlers on native elements.
useMemo helps when...
Measurably expensive computation (Profiler!) or object reference stabilization for a React.memo child. Not for trivial expressions.
React.memo is the key
Without React.memo on the child component, useCallback and useMemo have no effect on props. All three together or not at all.
React Compiler
In React 19, the compiler takes over automatic memoization. Measure first, only intervene manually where the Profiler shows a real problem.