DEV Community

loading...

Why I almost always`useMemo` and `useCallback` 🤯

Andy Richardson
I nerd out over React, JavaScript, Docker, Linux and Open Source. Maintainer of Tipple, Fielder, Urql & Urql Devtools.
Updated on ・3 min read

When your app starts to get slow, it's not a bad idea to start looking into optimizing your code.

This is a sentiment most of us follow (myself included) to avoid falling into the trap of premature optimization.

When I first started using React with hooks, I was very much in the mindset that the memoization hooks (useMemo and useCallback) could be spared for this reason. Over time however, after building libraries and applications that use hooks, I've found it almost always makes sense to memoize your code....

Here's why hooks are more than just a performance optimization.

What is a performance optimization

When we optimize code, the intention is to reduce the cost (time or resource usage). Particularly when we're optimizing functions or sections of our app, we don't expect the functionality to change, only the implementation.

The below is an example of a hook we which keeps the same functionality but changes it's implementation.

// Before optimization
const useArrayToObject = (array) => {
  return array.reduce((obj, value) => ({ [value]: true }), {});
}

// After optimization
const useArrayToObject = (array) => {
  const newCollection = {};

  for (let value in array) {
    newCollection[value] = true;
  }

  return newCollection
}

useMemo as a performance optimization

Now consider we find ourselves using this hook, and despite our prior optimization, we find that we need to further reduce it's cost.

As you can probably guess, we can make use of useMemo to ensure that the we only run our expensive operation whenever the input argument changes

const useArrayToObject = (array) => {
  return useMemo(() => {
    const newCollection = {};

    for (let value in array) {
      newCollection[value] = true;
    }

    return newCollection
  }, [array])
}

We merge the changes with confidence that our new optimization has solved the problem, only to hear later on that it's caused a new bug... but how?

The functional impact of useMemo

Despite intending to make a performance-only optimization by memoizing our hook, we've actually changed the way our app functionally works.

This issue can work both ways - either by adding memoization (sometimes inevitable) or removing it.

Here's the component which was affected by our change

const MyComponent = ({ array, dispatch, ...otherProps}) => {
  const collection = useArrayToObject(array);

  useEffect(() => {
    console.log('Collection has changed'); // Some side effect
  }, [collection])

  // ...
}

Unlike in the first example, the performance optimizations we have made to the internals of our hook have now changed how consuming components function.

Communicating change

The way in which changes cascade in React hooks is incredibly useful for making a reactive application. But, failing to communicate these changes up front, or modifying when these changes are communicated at a later date, can lead to lost (as in our example) or unintentional reactions elsewhere in your application.

The larger your application and the higher up in your component tree the modifications are, the larger the impact.

Addressing these issues

So now that you understand that useMemo does more than just optimize performance, here's my suggestion

just get into the habit of memoizing values.

Most are not going to notice the performance impact of additional equality checks incited by over-memoizing; and knowing that change events signalled by values coming from props or hooks can be trusted as actual changes, is valuable.

Update: I've added an example reproduction here demonstrating the functional impact of useMemo

Discussion (1)

Collapse
iegik profile image
Arturs Jansons

The below is an example of a hook we which keeps the same functionality but changes it's implementation.

  • bad example, where functionality is changes too. The better will be:
const useArrayToObject = (array) => ({ [array[array.length]]: true })
Enter fullscreen mode Exit fullscreen mode