The Memoization Cargo Cult
A pattern I see constantly in junior and mid-level React codebases: useCallback wrapped around every event handler, useMemo applied to every computed value, React.memo decorating every component. The developer's intent is performance optimization. The actual result is a codebase with more complexity, harder-to-read code, and — paradoxically — often worse performance than the naive version.
This happens because of a fundamental misunderstanding of what memoization costs. On every render, React must allocate memory for the dependencies array, compare each dependency to its previous value using Object.is, and either return the cached value or recompute it. If the savings from avoiding a recomputation are smaller than the overhead of performing these checks, memoization makes things slower.
Understanding When React Re-Renders
A component re-renders in three situations: its own state changes, its parent re-renders (the most common cause of unnecessary re-renders), or a context it subscribes to changes. When a parent component re-renders, every function defined inside the component is recreated as a new object reference. This is why child components wrapped in React.memo still re-render when their props are functions — the prop passes a reference that changes on every parent render.
useCallback: The Two Cases Where It Actually Helps
Case 1: Prop to a memoized child component. If you are passing a function as a prop to a child component wrapped in React.memo, you need useCallback to stabilize the function reference. Without it, the memo wrapper is useless.
Case 2: useEffect dependency. If a function is listed as a dependency in a useEffect, and that function is recreated on every render, the effect runs on every render — often causing infinite loops. useCallback stabilizes the function reference.
In all other situations — click handlers on HTML elements, event handlers not passed to memoized children — useCallback adds cost and complexity without benefit.
useMemo: Expensive Computations Only
useMemo is designed for computationally expensive operations where recalculating on every render is measurably slower than the memoization overhead. For most frontend operations — filtering arrays of 20 items, formatting a date, concatenating strings — the answer is definitively: do not memoize.
// Appropriate — filtering 10,000 products is expensive
const filteredProducts = useMemo(() => {
return allProducts.filter(p => p.category === selectedCategory && p.price <= maxPrice);
}, [allProducts, selectedCategory, maxPrice]);
// Wasteful — formatting a name is essentially free
const displayName = useMemo(() => `${firstName} ${lastName}`, [firstName, lastName]);
The Real Solution: State Colocation
In my experience, the root cause of React performance problems is almost never missing memoization — it is state stored too high in the component tree. Moving state down to the component that actually needs it eliminates the re-render cascade completely, with no memoization required.
Conclusion
Write clean, simple React code first. Measure performance using the React Profiler when users report slowness. Apply useCallback only when stabilizing function references for memoized child components or useEffect dependencies. Apply useMemo only for genuinely expensive computations verified by profiling. In 90% of React performance problems, the correct solution is better state architecture, not more memoization.
