Shelf

React useMemo — An Undecorated Soldier

By James Akpan

3 min read

Before the early months of 2019, rumours about React releasing a feature called "Hooks" had taken center stage in the frontend community. Some speculated its release would mean the death of the component lifecycle, while others felt it was just another fancy word. And then it happened. React Hooks were officially introduced with the release of React 16.8 in February 2019.

In the hearts and minds of millions of developers, Hooks like useEffect, useState, useContext, useReducer, useCallback, and useMemo quickly gained prominence. Over the following months, hundreds of articles praising this development were published across web development blogs. Codebases were refactored, and pull requests improving these Hooks were contributed. The enthusiasm was palpable.

But as the hype eventually died down, the Hooks remained a staple in developers' toolkits. However, one of these Hooks was left, as it were, undecorated.

The reduced usage of useMemo isn't in fact because of its efficacy. Reduced usage of useMemo is one of the bittersweet caveats of how fast browsers have become in computing. To understand this, let us first understand what useMemo was intended for; this is the sole purpose of this article.

In simple terms, the singular purpose of useMemo is to ensure that an intensive computation on the client-side is executed only once, and is re-run only when absolutely necessary, based on its dependencies.

In other words, useMemo, as the name suggests, memoizes and returns the result of this computation and will not re-run the computation unless its dependencies dictate otherwise.

I like to call this: CCO — Compute And Calculate Once.

Let us see this in action.

const jobOpenings = [
  { id: 1, title: 'Software Engineer', department: 'Engineering', location: 'New York' },
  { id: 2, title: 'Product Manager', department: 'Product', location: 'San Francisco' },
  { id: 3, title: 'Designer', department: 'Design', location: 'Remote' },
  { id: 4, title: 'Data Analyst', department: 'Engineering', location: 'New York' },
  // More job openings...
];

// States updated by what users type in respective search box.
const [department, setDepartment] = useState('');
const [location, setLocation] = useState('');

// To filter the jobOpenings object. We could ignore useMemo and do this
const filteredJobs = jobOpenings.filter(
  job =>
    (department === '' || job.department.includes(department)) &&
    (location === '' || job.location.includes(location))
);

This looks simple and almost industry standard. But what happens when the jobOpenings array becomes larger? Well, that too isn't the problem. The issue begins when your component re-renders; a text is updated, the theme is changed, or the "Show more" button is clicked. All these unrelated actions will cause your filtering computation to rerun, and that is where the real problem lies.

With our newfound knowledge of useMemo, let us now bring optimization to this component:

const jobOpenings = [
  { id: 1, title: 'Software Engineer', department: 'Engineering', location: 'New York' },
  { id: 2, title: 'Product Manager', department: 'Product', location: 'San Francisco' },
  { id: 3, title: 'Designer', department: 'Design', location: 'Remote' },
  { id: 4, title: 'Data Analyst', department: 'Engineering', location: 'New York' },
  // More job openings...
];

// States updated by what users type in respective search box.
const [department, setDepartment] = useState('');
const [location, setLocation] = useState('');

// Here we use useMemo
const filteredJobs = useMemo(() => {
  return jobOpenings.filter(
    job =>
      (department === '' || job.department.includes(department)) &&
      (location === '' || job.location.includes(location))
  );
}, [department, location]);

As long as the dependencies don't change and the component isn't unmounted, our filtering computation will run only once.

Conclusion

A simple rule of thumb for determining if useMemo is needed is to ask yourself:

  • Is this computation expensive or likely to become so?
  • Do I need to run this computation once and only update it when specific dependencies change?
  • Will recalculating this result on every render impact performance?
  • Is this result used in rendering or passed to child components?

If your answer is "Yes" to any of the above questions, then without a doubt you need useMemo().