React re-renders everything by default. When a parent component updates its state, every child component re-renders, even if its props have not changed. For small applications, this is invisible. For complex dashboards, data tables, and real-time UIs, it is the difference between a snappy interface and a sluggish one.
At Boundev, we have optimised over 90 React applications for production performance. The pattern is consistent: most performance problems are caused by unnecessary re-renders, and memoization is the surgical tool that eliminates them. But memoization is not a blanket solution. Applied incorrectly, it adds complexity without benefit. Here is how to use it precisely.
The Three Memoization Tools
Each tool caches something different. Using the wrong one for the job is a common source of bugs.
Why Re-Renders Happen
Before understanding memoization, you need to understand what triggers a re-render. React has three re-render triggers, and confusing them leads to misapplied optimizations.
State Changes
When a component calls setState or a state-updating function from useState, that component re-renders. This is intentional and correct. The component's output depends on its state, so React needs to recalculate the JSX.
Parent Re-Renders
When a parent re-renders, all children re-render by default, regardless of whether their props changed. This is the re-render that memoization targets. A parent updating a counter should not cause an unrelated child component to re-render, but without React.memo, it does.
Context Changes
When a context value changes, every component consuming that context re-renders. React.memo cannot prevent this. If your context provides a large object that changes frequently, every consumer re-renders even if they only use one property. This is why context should be split by concern.
Referential Equality: The Core Concept
Referential equality is the reason memoization matters in React. JavaScript compares primitives (strings, numbers, booleans) by value, but compares objects, arrays, and functions by reference. Two identical-looking objects created separately are not equal.
New Reference Every Render (Breaks Memoization):
const style = { color: 'red' } inside a component body creates a new object reference on every renderconst handleClick = () => {} creates a new function reference on every renderconst items = data.filter(...) creates a new array even if data has not changedStable Reference (Memoization Works):
const style = useMemo(() => ({ color: 'red' }), []) returns the same referenceconst handleClick = useCallback(() => {}, [deps]) preserves function identityconst items = useMemo(() => data.filter(...), [data]) recalculates only when data changesThis is the mental model that our dedicated React teams use when optimizing: if a value is passed as a prop to a memoized child, that value's reference must be stable. Otherwise, the memoization is useless.
React.memo: Memoizing Components
React.memo is a higher-order component that wraps a functional component and prevents it from re-rendering when its props are shallowly equal to the previous render's props. It is the first tool to reach for when a child component re-renders unnecessarily.
1When to Use React.memo
Use it on components that render frequently with the same props, that are expensive to render (complex calculations, large DOM trees), or that appear in lists where parent state changes frequently. Dashboard widgets, table rows, and list items are prime candidates.
2When NOT to Use React.memo
Do not wrap components that always receive new props (defeating the comparison), components that are cheap to render (the comparison overhead exceeds the render cost), or components that update their own state frequently (they re-render regardless of memo).
3Custom Comparison Function
Pass a second argument to React.memo for custom comparison logic: React.memo(Component, (prevProps, nextProps) => { /* return true to skip re-render */ }). Use this when you need deep comparison on specific props, but be aware that deep comparison can be expensive itself.
useMemo: Memoizing Computed Values
useMemo caches the result of a computation and only recalculates when its dependencies change. It reduces the amount of work done during a render by avoiding redundant expensive calculations.
Expensive Calculations—Sorting, filtering, or transforming large datasets should be wrapped in useMemo so the computation only runs when the source data changes, not on every keystroke or scroll event.
Referential Stability—Objects or arrays passed as props to memoized children must maintain the same reference. useMemo ensures the reference stays stable if dependencies have not changed.
Dependency Arrays Matter—Missing a dependency causes stale data bugs. Adding unnecessary dependencies triggers recalculation too often. Lint rules (react-hooks/exhaustive-deps) catch most mistakes.
Do Not Memoize Trivial Ops—Adding two numbers or concatenating a string is faster than the memoization overhead. Reserve useMemo for computations that measurably impact render time.
useCallback: Memoizing Functions
useCallback returns a memoized version of a callback function that only changes when its dependencies change. Its primary purpose is preserving function reference identity so that memoized child components do not re-render due to a new function reference.
When useCallback Makes a Difference
useCallback only adds value when the function is used as a dependency or prop for a memoized consumer.
React.memo component, wrapping it in useCallback prevents the child from re-rendering when the parent's unrelated state changesuseEffect or useMemo, useCallback prevents those hooks from re-running on every renderuseCallback prevents the debounce timer from resetting on every renderuseCallback adds overhead without benefitNeed React Performance Engineering?
Boundev engineers profile, diagnose, and fix React performance issues. From re-render storms to bundle bloat, we make applications fast.
Talk to Our React TeamThe Decision Framework: When to Memoize
Memoization is not free. Every useMemo and useCallback adds memory overhead and comparison logic. The question is never "should I memoize?" but "is the cost of re-rendering higher than the cost of memoizing?" Here is the decision framework.
1Profile First, Optimise Second
Open React DevTools Profiler. Record an interaction. Look at which components re-rendered and how long each render took. If a component renders in under 1ms and re-renders infrequently, memoization adds no measurable value. Focus on the components that appear repeatedly in the flame graph with significant render times.
2Identify the Re-Render Source
Is the component re-rendering because of its own state (memoization will not help), because of a context change (split the context), or because of a parent re-render (React.memo will help)? Diagnosing the source determines the solution.
3Apply the Correct Tool
Component re-renders unnecessarily? Use React.memo. Expensive computation runs on every render? Use useMemo. Function reference triggers child re-render? Use useCallback. Value is a primitive (string, number)? No memoization needed because primitives compare by value.
4Verify the Improvement
Profile again after applying memoization. If the re-render count and render time dropped, the optimisation worked. If nothing changed, remove the memoization because it is adding complexity without benefit.
Real-World Impact: A SaaS dashboard client was experiencing 2-second lag on every filter change because their data table (300+ rows) re-rendered entirely on every keystroke. Through our frontend development partnership, we wrapped table rows in React.memo, memoized the filter computation with useMemo, and stabilised the onChange handler with useCallback. Filter response dropped from 2,100ms to 45ms. Development cost: $3,700. Estimated churn prevention: $11,500/mo.
Common Memoization Mistakes
Memoization applied incorrectly is worse than no memoization at all because it adds complexity and a false sense of security. These are the mistakes we see most often in codebases.
Anti-Patterns to Avoid
Patterns that create the illusion of optimization while delivering no benefit or causing bugs.
React.memo while passing an inline object or function as a prop; the new reference on every render defeats the memo entirelyReact.memo and every value in useMemo adds memory overhead and comparison cost everywhere, often exceeding the cost of the renders being preventeduseMemo is for pure computations only; side effects (API calls, event subscriptions) belong in useEffectReact.memo comparison already works by value; the memo adds overhead without preventing any re-rendersWhen augmented frontend engineers from Boundev join a project, one of the first audits is the memoization layer. We consistently find 30-40% of useMemo/useCallback calls in existing codebases are unnecessary, adding complexity without measurable performance benefit.
Frequently Asked Questions
What is the difference between useMemo and useCallback?
useMemo caches a computed value and returns the cached result when dependencies have not changed. useCallback caches a function reference and returns the same function instance across renders. Conceptually, useCallback(fn, deps) is equivalent to useMemo(() => fn, deps). Use useMemo for expensive calculations and object/array creation. Use useCallback for event handlers and callbacks passed to memoized child components.
When should I use React.memo?
Use React.memo when a component re-renders frequently due to parent state changes even though its own props have not changed, and when that component is expensive to render. Common candidates include table rows, list items, chart components, and dashboard widgets. Always profile first with React DevTools to confirm the component is actually re-rendering unnecessarily before adding React.memo.
Does React.memo prevent all re-renders?
No. React.memo only prevents re-renders caused by parent re-renders when props have not changed. A component wrapped in React.memo will still re-render if its own state changes, if a context it consumes updates, or if any prop reference changes (including inline objects, arrays, or functions created during the parent's render). Ensuring prop referential stability with useMemo and useCallback is essential for React.memo to work.
Can over-memoization hurt performance?
Yes. Every useMemo and useCallback call stores a cached value in memory and runs a dependency comparison on every render. For trivial computations (string concatenation, simple math), the comparison overhead exceeds the computation cost, making the memoization a net negative. Over-memoization also increases code complexity, making the codebase harder to maintain and debug.
What is referential equality and why does it matter for React?
Referential equality means two variables point to the same object in memory. JavaScript compares objects, arrays, and functions by reference, not by content. In React, this matters because React.memo uses shallow comparison to check if props changed. Two objects with identical content but different references (e.g., both are { color: 'red' }) are considered different, triggering a re-render. useMemo and useCallback preserve references across renders to maintain referential equality.
