Performance Optimization in React: The Essential Role of useMemo
React performance work often comes down to not redoing expensive work between renders. The useMemo hook memoizes the return value of a function so a component only recomputes when its dependencies actually change. This post walks through how useMemo works, when to reach for it, and how it solves the referential equality problems that quietly cause unnecessary re-renders.
1. Definition
The useMemo hook lets you memoize the return value of a function. It takes two arguments: a function that returns a value, and an array of dependencies. If the dependencies haven’t changed between renders, useMemo skips the function and hands back the previously computed value. That’s the whole point of memoization.
It’s especially useful for deterministic, expensive functions and for working around referential equality problems.
Here’s an example:
import { useMemo } from 'react'
function ExpensiveComponent({ value }) {
const computedValue = useMemo(() => {
return value + 123 // assume this is an expensive computation...
}, [value])
return <div>{computedValue}</div>
}In this minimal example, useMemo keeps the (presumably expensive) computation from rerunning every render.
2. Reference
const cachedValue = useMemo(functionToMemoize, dependencies)functionToMemoize: A function that returns a value. This function is only re-executed when one or more dependencies change.dependencies: An array of dependencies that determine when to re-compute thefunctionToMemoize. If any value in this array changes, the function will be re-executed; otherwise, the cached value is returned.cachedValue: The memoized result of the function.
3. Referencial Equality Problems
In JavaScript, objects are compared by reference, not by the values of their properties. Two object variables are equal only when they point to the same location in memory.
Consider the following example:
const object1 = {
prop1: 1,
prop2: "somevalue"
}
const object2 = {
prop1: 1,
prop2: "somevalue"
}
// the condition "object1 == object2" will evaluate to FALSESame shape, same values, different memory addresses. JavaScript treats them as distinct.
In React this bites every render. The objects a component builds locally are constructed fresh, at new addresses, even when their contents are byte-for-byte identical to the previous render. Anything downstream that depends on those objects sees them as changed and recomputes.
Two ways out. Use value types (strings, numbers or booleans) as dependencies wherever you can, or stabilize the object references by memoizing them.
Stabilizing Object Dependencies with useMemo
If you need to pass an object as a dependency to useEffect or useMemo, wrap its construction in a useMemo of its own. The address stays put between renders as long as the inner dependencies don’t change:
const themeStyles = useMemo(() => {
return {
backgroundColor: theme == "dark" ? "black" : "white"
//...
}
}, [theme])
//...
return (
<div style={themeStyles}>wow, that works</div>
)themeStyles holds CSS rules derived from a theme state variable. Memoizing it means the object survives renders unchanged until theme itself flips.
4. Memoizing Objects
If an object shows up as a dependency, memoize it to stop the per-render reinitialization. Reach for useMemo here, not useCallback. useMemo is designed to cache a value (an object, a computed result, anything that isn’t a function):
const memoizedObject = useMemo(() => ({
prop1: dep1,
prop2: dep2,
}), [dep1, dep2])As long as dep1 and dep2 are stable, the same object reference threads through every render.
Don’t try to do this with useCallback; that hook returns a memoized function, not an object.
5. Caveats
Once you’ve seen useMemo save a render, the temptation is to sprinkle it everywhere, even on trivial functions. Don’t. Memoization isn’t free; the cached values sit in memory and the dependency comparison runs every render. Use it where the work it skips costs more than the bookkeeping it adds.