DEV Community

Cover image for You’re overusing useMemo: Rethinking Hooks memoization
Brian Neville-O'Neill
Brian Neville-O'Neill

Posted on • Originally published at blog.logrocket.com on

You’re overusing useMemo: Rethinking Hooks memoization

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.

Category A And Category B: 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.

LogRocket Free Trial Banner

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}/> 
}
Enter fullscreen mode Exit fullscreen mode

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}/> 
}
Enter fullscreen mode Exit fullscreen mode

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 /> 
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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 /> 
}
Enter fullscreen mode Exit fullscreen mode

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 /> 
}
Enter fullscreen mode Exit fullscreen mode

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

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} />                 
}
Enter fullscreen mode Exit fullscreen mode

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.

Initial Props And Updated Props Example

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
}, [])
Enter fullscreen mode Exit fullscreen mode

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} />   
}
Enter fullscreen mode Exit fullscreen mode

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} />
}
Enter fullscreen mode Exit fullscreen mode

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} />
}
Enter fullscreen mode Exit fullscreen mode

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], [])
Enter fullscreen mode Exit fullscreen mode

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} />
}
Enter fullscreen mode Exit fullscreen mode

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 Dashboard Free Trial Banner
 
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)

Collapse
 
dance2die profile image
Sung M. Kim

Thanks, Brian.

This post has been quite an eye-opener 👀