Learn React useMemo with real examples, performance optimization tips, and differences with useCallback. Master memoization and avoid unnecessary re-renders.
Picture a calculator with a memory button. You punch in a complicated calculation, get the result, and press M+ to store it. Next time someone asks for that same calculation with the same inputs, you just recall the memory — no recalculating needed. But if the inputs change, the memory is stale and you recalculate from scratch. useMemo is that memory button for React components. It stores the result of a computation and returns the stored result on subsequent renders — as long as the dependencies haven't changed. When a dependency changes, the computation runs again and the new result is stored. But here is the thing most developers miss: the memory button has a cost. Every time the component renders, React still checks whether the dependencies changed. That check costs something. For simple computations that are cheaper than a comparison, adding useMemo actually makes things slower, not faster. useMemo has a second, equally important purpose beyond performance: referential stability. When you create an object or array inside a component, you get a brand new reference every render, even if the contents are identical. useMemo lets you return the same reference when inputs haven't changed — which is what makes React.memo and useEffect dependency comparisons work correctly.
useMemo memoizes the result of a computation. The function you pass is only called when the dependencies change. Between dependency changes, React returns the cached result.
const memoizedValue = useMemo(() => {
return expensiveComputation(a, b)
}, [a, b])
// First render: calls expensiveComputation(a, b), stores result
// Re-render, same a and b: returns cached result immediately
// Re-render, a changed: calls expensiveComputation(newA, b), stores new result
// Without useMemo — re-sorts on every render, even unrelated renders
function ProductList({ products, sortBy, searchQuery }) {
const [count, setCount] = useState(0) // unrelated state
// This runs even when count changes — wasteful for 10,000 products
const sorted = [...products].sort((a, b) =>
sortBy === 'price' ? a.price - b.price : a.name.localeCompare(b.name)
)
return <ul>{sorted.map(p => <Item key={p.id} product={p} />)}</ul>
}
// With useMemo — only re-sorts when products or sortBy changes
function ProductList({ products, sortBy, searchQuery }) {
const [count, setCount] = useState(0)
const sorted = useMemo(() => {
console.log('sorting...') // only logs when products or sortBy changes
return [...products].sort((a, b) =>
sortBy === 'price' ? a.price - b.price : a.name.localeCompare(b.name)
)
}, [products, sortBy])
return <ul>{sorted.map(p => <Item key={p.id} product={p} />)}</ul>
}
React.memo does a shallow comparison of props. If you pass an object or array created inline, the reference changes every render — React.memo can never skip.
const HeavyChart = React.memo(({ config }) => {
// expensive rendering
return <Chart {...config} />
})
// ✗ Defeats React.memo — new config object every render
function Dashboard() {
const config = { color: 'blue', type: 'bar', animate: true }
return <HeavyChart config={config} /> // HeavyChart re-renders always!
}
// ✓ Stable reference — React.memo can skip re-renders
function Dashboard() {
const config = useMemo(() => ({
color: 'blue', type: 'bar', animate: true
}), []) // stable — empty deps, never changes
return <HeavyChart config={config} /> // HeavyChart skips re-renders ✓
}
useEffect uses the same reference comparison for its dependency array. An object created inline changes reference every render — the effect runs every render.
// ✗ Infinite loop — fetchConfig is new every render
function UserDashboard({ userId }) {
const fetchConfig = { headers: { 'X-User': userId }, timeout: 5000 }
useEffect(() => {
fetchUserData(userId, fetchConfig)
}, [userId, fetchConfig]) // fetchConfig is new every render → infinite loop!
}
// ✓ Stable reference — effect runs only when userId changes
function UserDashboard({ userId }) {
const fetchConfig = useMemo(() => ({
headers: { 'X-User': userId },
timeout: 5000
}), [userId]) // new reference only when userId changes
useEffect(() => {
fetchUserData(userId, fetchConfig)
}, [userId, fetchConfig]) // ✓ runs only when userId changes
}
useMemo itself has a cost on every render: allocates memory for the cache, runs the dependency comparison, and retains the old value in memory. For simple computations, the memoization overhead may exceed the computation cost.
// ✗ Over-memoization — useMemo costs MORE than the compute
const fullName = useMemo(() => first + ' ' + last, [first, last])
// String concat is O(n) microseconds. useMemo comparison is O(m) where m = deps.length.
// Net result: slower.
// ✓ Compute inline — faster
const fullName = first + ' ' + last
// ✗ Memoizing primitive values — pointless
const doubled = useMemo(() => count * 2, [count])
// Numbers are compared by value — React already handles this efficiently
// ✓ Worth memoizing
const chartData = useMemo(() => {
return rawData
.filter(d => d.date >= startDate && d.date <= endDate)
.map(d => ({ x: d.date, y: d.value }))
.reduce(/* ... aggregation ... */)
}, [rawData, startDate, endDate]) // O(n) with significant constant — worth caching
// useMemo — memoizes the RESULT of calling a function
const processedData = useMemo(() => expensiveTransform(rawData), [rawData])
// processedData is a value (array, object, number, etc.)
// useCallback — memoizes the FUNCTION ITSELF
const handleChange = useCallback(e => setValue(e.target.value), [])
// handleChange is a stable function reference
// They are literally equivalent:
useCallback(fn, deps) === useMemo(() => fn, deps)
// You could implement useCallback with useMemo:
const stableFn = useMemo(() => (...args) => doSomething(...args), [dep])
// JavaScript compares objects by reference, not content
{ a: 1 } === { a: 1 } // false — different objects in memory
[1, 2, 3] === [1, 2, 3] // false — different arrays
() => {} === () => {} // false — different functions
// React.memo and useEffect deps use Object.is — same as ===
// This is why inline objects/arrays always "change" between renders
The React Compiler (shipping in React 19) automatically applies memoization at the compiler level. It analyses your component code and inserts useMemo/useCallback calls where they would be beneficial — without you writing them manually.
// What you write (no memoization):
function ProductList({ products, sortBy }) {
const sorted = products.slice().sort((a, b) => a[sortBy] - b[sortBy])
return <ul>{sorted.map(p => <Item key={p.id} product={p} />)}</ul>
}
// What React Compiler produces (conceptually):
function ProductList({ products, sortBy }) {
const sorted = useMemo(() => products.slice().sort((a, b) => a[sortBy] - b[sortBy]), [products, sortBy])
return <ul>{sorted.map(p => <Item key={p.id} product={p} />)}</ul>
}
With the React Compiler enabled, manually written useMemo calls become largely unnecessary. This is why the advice is: write clean code first, add useMemo only where profiling confirms a bottleneck — the compiler will handle the rest.
Many developers think useMemo always makes components faster — but it adds overhead on every render (comparison + cache lookup). For trivial computations, useMemo makes things slower. It's only beneficial when the memoized computation cost clearly exceeds the memoization overhead — which requires profiling to confirm.
Many developers think useMemo is only for expensive computations — but its second major use is referential stability: returning the same object/array/function reference when inputs haven't changed. This is what makes React.memo comparisons and useEffect dependency arrays work correctly for non-primitive values.
Many developers think useMemo(fn, []) caches forever — it does for the lifetime of that component instance. But when the component unmounts and remounts, the cache is cleared and the computation runs fresh. Two different mounted instances of the same component each have their own independent memo cache.
Many developers think useMemo prevents the wrapped function from running on the first render — but useMemo always runs the function on the first render to compute the initial value. There is no "skip first render" option. Memoization only kicks in from the second render onward.
Many developers think useMemo is equivalent to a selector library like Reselect — Reselect creates a memoized selector that works outside components, can be shared across instances, and has a configurable cache size. useMemo is component-local, per-instance, and has a cache size of 1. For global computed state, use Reselect or Zustand selectors.
Many developers think they should add useMemo to every computed value by default — but this is premature optimization. React's rendering is fast. Write code without useMemo first, profile if you see slowness, and add useMemo only where measurements confirm it helps.
Large data grids at Flipkart and Meesho filter and sort tens of thousands of product rows. Without useMemo, the entire sort runs on every keystroke, every hover, every unrelated state change. With useMemo keyed to the sort column and filter values, the computation runs only when those inputs change — making the grid feel instant.
Chart libraries like Recharts and Chart.js receive config objects as props. Without memoizing the config, the chart re-renders on every parent render even when the underlying data hasn't changed. The chart library can't tell that { color: 'blue' } this render is the same as { color: 'blue' } last render — it only sees a new reference.
Autocomplete and search-as-you-type components use useMemo to compute filtered results from a local dataset. The filter runs on every keystroke through the search term dependency, but not on other unrelated state changes like loading indicators or modal states.
Permission and role-based UI uses useMemo to compute what features a user can access: const permissions = useMemo(() => computePermissions(user.role, user.overrides), [user.role, user.overrides]). This prevents the permission computation from running on every render cycle, and ensures the permissions object has a stable reference for useEffect dependencies.
Design system component libraries (like Radix, shadcn) memoize style objects internally so that components wrapping native HTML elements don't pass new style prop references on every render — which would force browsers to recalculate CSS needlessly.
No questions tagged to this topic yet.
Reading answers is not the same as knowing them. Practice saying them out loud with AI feedback — that's what builds real interview confidence.