Written by Ohans Emmanuel✏️
In my experience, there are two main categories where I’ve found useMemo
to be irrelevant, overused, and likely harmful to the performance of your application.
The first category is easy to reason about; however, the second category is quite subtle and easily ignored. If you’ve used Hooks in any serious production app, then you’ve likely been tempted to use the useMemo
Hook in one of these two categories.
I’ll show you why these are unimportant and likely hurting the performance of your application, and more interestingly, I’ll show you my recommendations on how not to overuse useMemo
in these use cases.
Shall we get started?
Where not to use useMemo
The classifications, for the sake of learning purposes, will be called Lions and Chameleons.
Ignore the distracting classification monikers and hang on!
Your immediate reaction when confronted by a lion is to run away, protect your heart from being ripped apart, and live to tell the story later. There’s no time for chitter-chatter.
This is category A. They are lions, and your reaction should be to run away from these.
Let’s start with these before looking at the more subtle chameleons.
1. Same reference and inexpensive operations
Consider the example component below:
/**
@param {number} page
@param {string} type
**/
const myComponent({page, type}) {
const resolvedValue = useMemo(() => {
getResolvedValue(page, type)
}, [page, type])
return <ExpensiveComponent resolvedValue={resolvedValue}/>
}
In this example, it’s easy to justify the writer’s use of useMemo
. What goes through their mind is they don’t want the ExpensiveComponent
to be re-rendered when the reference to resolvedValue
changes.
While that’s a valid concern, there are two questions to ask to justify the use of useMemo
at any given time.
First, is the function passed into useMemo
an expensive one? In this case, is the getResolvedValue
computation an expensive one?
Most methods on JavaScript data types are optimized, e.g. Array.map
, Object.getOwnPropertyNames()
, etc. If you’re performing an operation that’s not expensive (think Big O notation), then you don’t need to memoize the return value. The cost of using useMemo
may outweigh the cost of reevaluating the function.
Second, given the same input values, does the reference to the memoized value change? For example, in the code block above, given the page
as 2
and type
as "GET"
, does the reference to resolvedValue
change?
The simple answer is to consider the data type of the resolvedValue
variable. If resolvedValue
is a primitive
(i.e., string
, number
, boolean
, null
, undefined
, or symbol
), then the reference never changes. By implication, the ExpensiveComponent
won’t be re-rendered.
Consider the revised code below:
/**
@param {number} page
@param {string} type
**/
const MyComponent({page, type}) {
const resolvedValue = getResolvedValue(page, type)
return <ExpensiveComponent resolvedValue={resolvedValue}/>
}
Following the explanation above, if resolvedValue
returns a string or other primitive value, and getResolvedValue
isn’t an expensive operation, then this is perfectly correct and performant code.
As long as page
and type
are the same — i.e., no prop changes — resolvedValue
will hold the same reference except the returned value isn’t a primitive (e.g., an object or array).
Remember the two questions: Is the function being memoized an expensive one, and is the returned value a primitive? With these questions, you can always evaluate your use of useMemo
.
2. Memoizing default state for any number of reasons
Consider the following code block:
/**
@param {number} page
@param {string} type
**/
const myComponent({page, type}) {
const defaultState = useMemo(() => ({
fetched: someOperationValue(),
type: type
}), [type])
const [state, setState] = useState(defaultState);
return <ExpensiveComponent />
}
The code above seems harmless to some, but the useMemo
call there is absolutely unimportant.
First, out of empathy, understand the thinking behind this code. The writer’s intent is laudable. They want a new defaultState
object when the type
prop changes, and they don’t want reference to the defaultState
object to be invalidated on every re-render.
While these are decent concerns, the approach is wrong and violates a fundamental principle: useState
will not be reinitialized on every re-render, only when the component is remounted.
The argument passed to useState
is better called INITIAL_STATE
. It’s only computed (or triggered) once when the component is initially mounted.
useState(INITIAL_STATE)
Even though the writer is concerned about getting a new defaultState
value when the type
array dependency for useMemo
changes, this is a wrong judgment as useState
ignores the newly computed defaultState
object.
This is the same for lazily initializing useState
as shown below:
/**
@param {number} page
@param {string} type
**/
const myComponent({page, type}) {
// default state initializer
const defaultState = () => {
console.log("default state computed")
return {
fetched: someOperationValue(),
type: type
}
}
const [state, setState] = useState(defaultState);
return <ExpensiveComponent />
}
In the example above, the defaultState
init function will only be invoked once — on mount. The function isn’t invoked on every re-render. As a result, the log “default state computed” will only be seen once, except the component is remounted.
Here’s the previous code rewritten:
/**
@param {number} page
@param {string} type
**/
const myComponent({page, type}) {
const defaultState = () => ({
fetched: someOperationValue(),
type,
})
const [state, setState] = useState(defaultState);
// if you really need to update state based on prop change,
// do so here
// pseudo code - if(previousProp !== prop){setState(newStateValue)}
return <ExpensiveComponent />
}
We will now consider what I deem more subtle scenarios where you should avoid useMemo
.
3. Using useMemo
as an escape hatch for the ESLint Hook warnings
While I couldn’t bring myself to read all the comments from people who seek ways to suppress the lint warnings from the official ESLint plugin for Hooks, I do understand their plight.
I agree with Dan Abramov on this one. Suppressing the eslint-warnings
from the plugin will likely come back to bite you someday in the future.
Generally, I consider it a bad idea to suppress these warnings in production apps because you increase the likelihood of introducing subtle bugs in the near future.
With that being said, there are still some valid cases for wanting to suppress these lint warnings. Below is an example I’ve run into myself. The code’s been simplified for easier comprehension:
function Example ({ impressionTracker, propA, propB, propC }) {
useEffect(() => {
// 👇Track initial impression
impressionTracker(propA, propB, propC)
}, [])
return <BeautifulComponent propA={propA} propB={propB} propC={propC} />
}
This is a rather tricky problem.
In this specific use case, you don’t care whether the props change or not. You’re only interested in invoking the track
function with whatever the initial props are. That’s how impression tracking works. You only call the impression track function when the component mounts. The difference here is you need to call the function with some initial props.
While you may think simply renaming the props
to something like initialProps
solves the problem, that won’t work. This is because BeautifulComponent
relies on receiving updated prop values, too.
In this example, you will get the lint warning message: “React Hook useEffect has missing dependencies: ‘impressionTracker’, ‘propA’, ‘propB’, and ‘propC’. Either include them or remove the dependency array.”
That’s a rather brash message, but the linter is simply doing its job. The easy solution is to use a eslint-disable
comment, but this isn’t always the best solution because you could introduce bugs within the same useEffect
call in the future.
useEffect(() => {
impressionTracker(propA, propB, propC)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
My suggestion solution is to use the useRef
Hook to keep a reference to the initial prop values you don’t need updated.
function Example({impressionTracker, propA, propB, propC}) {
// keep reference to the initial values
const initialTrackingValues = useRef({
tracker: impressionTracker,
params: {
propA,
propB,
propC,
}
})
// track impression
useEffect(() => {
const { tracker, params } = initialTrackingValues.current;
tracker(params)
}, []) // you get NO eslint warnings for tracker or params
return <BeautifulComponent propA={propA} propB={propB} propC={propC} />
}
In all my tests, the linter only respects useRef
for such cases. With useRef
, the linter understands that the referenced values won’t change and so you don’t get any warnings! Not even useMemo
prevents these warnings.
For example:
function Example({impressionTracker, propA, propB, propC}) {
// useMemo to memoize the value i.e so it doesn't change
const initialTrackingValues = useMemo({
tracker: impressionTracker,
params: {
propA,
propB,
propC,
}
}, []) // 👈 you get a lint warning here
// track impression
useEffect(() => {
const { tracker, params} = initialTrackingValues
tracker(params)
}, [tracker, params]) // 👈 you must put these dependencies here
return <BeautifulComponent propA={propA} propB={propB} propC={propC} />
}
In the faulty solution above, even though I keep track of the initial values by memoizing the initial prop values with useMemo
, the linter still yells at me. Within the useEffect
call, the memoized values tracker
and params
still have to be entered as array dependencies, too.
I’ve seen people useMemo
in this way. It’s poor code and should be avoided. Use the useRef
Hook, as shown in the initial solution.
In conclusion, in most legitimate cases where I really want to silent the lint warnings, I’ve found useRef
to be a perfect ally. Embrace it.
4. Using useMemo
solely for referential equalities
Most people say to use useMemo
for expensive calculations and for keeping referential equalities. I agree with the first but disagree with the second. Don’t use the useMemo
Hook just for referential equalities. There’s only one reason to do this — which I discuss later.
Why’s using useMemo
solely for referential equalities a bad thing? Isn’t this what everyone else preaches?
Consider the following contrived example:
function Bla() {
const baz = useMemo(() => [1, 2, 3], [])
return <Foo baz={baz} />
}
In the component Bla
, the value baz
is memoized NOT because the evaluation of the array [1,2,3]
is expensive, but because the reference to the baz
variable changes on every re-render.
While this doesn’t seem to be a problem, I don’t believe useMemo
is the right Hook to use here.
One, look at the array dependency.
useMemo(() => [1, 2, 3], [])
Here, an empty array is passed to the useMemo
Hook. By implication, the value [1,2,3]
is only computed once — when the component mounts.
So, we know two things: the value being memoized is not an expensive calculation, and it is not recomputed after mount.
If you find yourself in such a situation, I ask that you rethink the use of the useMemo
Hook. You’re memoizing a value that is not an expensive calculation and isn’t recomputed at any point in time. There’s no way this fits the definition of the term “memoization.”
This is a terrible use of the useMemo
Hook. It is semantically wrong and arguably costs you more in terms of memory allocation and performance.
So, what should you do?
First, what exactly is the writer trying to accomplish here? They aren’t trying to memoize a value; rather, they want to keep the reference to a value the same across re-renders.
Don’t give that slimy chameleon a chance. In such cases, use the useRef
Hook.
For example, if you really hate the use of the current property (like a lot of my colleagues), then simply deconstruct and rename as shown below:
function Bla() {
const { current: baz } = useRef([1, 2, 3])
return <Foo baz={baz} />
}
Problem solved.
In fact, you can use the useRef
to keep reference to an expensive function evaluation — so long as the function doesn’t need to be recomputed on props change.
useRef
is the right Hook for such scenarios, NOT the useMemo
Hook.
Being able to use the useRef
Hook to mimic instance variables is one of the least used super powers Hooks avail us. The useRef
hook can do more than just keeping references to DOM nodes. Embrace it.
Please remember, the condition here is if you’re memoizing a value just because you need to keep a consistent reference to it. If you need the value to be recomputed based off of a changing prop or value, then please feel free to use the useMemo
hook. In some cases you could still use useRef
– but the useMemo
is mostly convenient given the array dependency list.
Conclusion
Run away from lions, but don’t let the chameleons fool you. If you allow them, the chameleons will change their skin colors, blend into your codebase, and pollute your code quality.
Don’t let them.
Curious what my stance on advanced Hooks is? I’m working on a video course for advanced Hooks. Sign up and I’ll let you know when it’s out!
Editor's note: Seeing something wrong with this post? You can find the correct version here.
Plug: LogRocket, a DVR for web apps
LogRocket is a frontend logging tool that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.
In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.
Try it for free.
The post You’re overusing useMemo: Rethinking Hooks memoization appeared first on LogRocket Blog.
Top comments (1)
Thanks, Brian.
This post has been quite an eye-opener 👀