jnystad.no About me

Not using useCallback is premature optimization

TL;DR;

It is extremely unlikely that overusing useCallback or useMemo is going to be the culprit in your app's performance (the opposite is more likely). Start somewhere else if you need to optimize (the profiler is your friend), and just add useCallback and useMemo to your muscle memory. It will pay off in the long run.

Senior is just a title

A thing I have noticed a bit more frequently lately is less knowledgeable but still experienced (in number of years) React developers' tendency of actually knowing how to use just two hooks: useState and useEffect. Some might also know useRef if they at some point tried to implement some DOM manipulation stuff (more often than not copied from SO) that probably should have been solved in CSS or otherwise.

useCallback and useMemo? Not so much.

One common pattern is the useEffect and useState combo for processing some data without any side effects, causing unnecessary render cycles between data change and computed value update. This is what useMemo is for.

// Contrived example, usually it's a little less obvious...
const [filteredData, setFilteredData] = useState();
useEffect(() => {
  setFilteredData(data.filter(x => isOfSomeType(x)));
}, [data]);
return <List data={filteredData} />;

// Do this instead
const filteredData = useMemo(() => {
  return data.filter(x => isOfSomeType(x));
}, [data]);
return <List data={filteredData} />;

Another is lying in the dependency arrays (and disabling that annoying lint rule) for various hooks since it was seemingly the only way to stop that useEffect from triggering the rate limiter of that API you were testing (we all do this from time to time, but removing the dependency is not the right solution).

Forget about "doing too much work", the primary reason useCallback and useMemo are your friends (and sometimes, useRef) is because they make those function, array and object references stable, and stable references work very well in dependency arrays.

But why are these so unknown among so many that seemingly should know better? I guess most people don't really read the docs (breaking news).

To their defence, both useCallback and useMemo are put in the "Additional Hooks" category in the reference, and are not mentioned in the "Hooks at a Glance" section at all. So I guess that is partially to blame. I think downplaying these hooks may have been a mistake. Although I would expect a "Senior React Developer" to have actually read the reference at some point.

The new React Docs (beta) have also not solved this. Yet. Still missing a lot there, and the stuff that's there seem quite good. Let's hope some great content about lesser known hooks appear before it's out of beta.

Premature optimization

I tried to Google a bit (with Brave Search, so I really need to unlearn Google as a verb) for this, and there seems to be a common misconception in articles targeted at beginners in React. It generally can be reduced to this: Using useCallback and useMemo is premature optimization, and should not be thrown around haphazardly as it may impact the performance of your app.

If fresh React developers even get this far they will probably be scared from using these hooks at all. After all "premature optimization is the root of all evil" (this expression is so often misused I think it may have caused more harm than gain at this point).

Well, here is some counter advice: Not using useCallback is premature optimization, and it may very well impact the performance of your app.

useMemo is a bit more nuanced, since it is no point in using it for trivial stuff and would only clutter your code. But if you need that calculated array or deduced object to be stable, feel free to wrap it in a useMemo. Just make sure its dependencies are stable too.

I recently came across a project where the developers must have just discovered useMemo and gotten some benefits from it, since it was littered with const xMemoed = useMemo(() => x, [x]).

Don't do this, if you are wondering, it is completely meaningless. xMemoed is exactly as (un)stable as x.

A function, array and object walks into a bar

There is one common case where you should not use useCallback and useMemo. If that function, array or object is actually pure/constant (i.e. not depending on any props or state in your component at all), just declare it outside the component.

// Don't do this
function MyBadComponent({ onSave }) {
  function isOfType(object, type) {
    return object.type === type;
  }

  return <SomeComponentThatMayRelyOnStableProps
    someDefault={{ x: 0, y: 0 }}
    onChange={(data) => {
      if (isOfType(data, "draft")) {
        onSave(data);
      }
    }}
  />;
}

// Do this instead

// This is just a static object and should not be declared
// inside the component
const someValues = { x: 0, y: 0 };

// This is just a pure helper function, and should also not be
// declared inside the component
function isOfType(object, type) {
  return object.type === type;
}

function MyGoodComponent({ onSave }) {
  // The onChange callback should be stable, but since it uses a
  // prop, it must be declared inside the component. Wrap it!
  const handleChange = useCallback((data) => {
    if (isOfType(data, "draft")) {
      onSave(data);
    }
  }, [onSave]);

  // Assuming onSave is stable itself, only stable references
  // are passed down
  return <SomeComponentThatMayRelyOnStableProps
    someDefault={someValues}
    onChange={handleChange}
  />;
}

Note the subtle name of the component that receives someSettings and handleChange. Sometimes, a stable reference to a function or object may not be necessary. But a lot of times, the child component may have complex logic itself that you may or may not be aware of.

Also, even if it doesn't right now, it may in the future. Would you rather pass stable references up front, costing a few dozen keystrokes or so (assuming you have some sort of auto complete), or would you rather risk breaking something in the future when SomeComponentThatMayRelyOnStableProps is modified, then hunting down all instances of unstable function and object references passed to this component?

What if SomeComponentThatMayRelyOnStableProps is in a library that you are not maintaining yourself?

I can guarantee that playing safe here will be worth it in the long run.

What about performance?

I've coded a lot of front end applications with React since I started experiencing with it some time in 2015. Both big and small, with lots of data and complex UIs (I work with web GIS, and believe me, getting interactive map libraries to play nice with React is not necessarily trivial).

Back then we had the component lifecycle to deal with, and checking prop changes to avoid unnecessary updates was both tedious and error prone (yes, PureComponent something, something).

Hooks was a blessing when it was introduced as we could stop worrying so much about the lifecycle of our components and focus more on what the component was actually doing, and with what. The trade-off is having to learn to use hooks correctly. But when learned -- and the need to "do this thing only on first mount" was unlearned -- the world (of React development) became a much better place.

With thousands of geometries in an interactive map, with lists and tables of items filtered by bounding box and categories, sorted by geodetic distance from wherever you were interested in, the performance issues that arose were never too many uses of useCallback or useMemo. The opposite caused issues multiple times however, of the app running haywire (or to a halt) kind.

The performance issues in React are almost always trying to do unnecessarily complex stuff, rendering ridiculous amounts of things, or performing actually heavy computations too often. And sometimes because you forgot that useCallback or useMemo in that overarching context provider (by the way, the scare mongering about contexts warrants its own post).

So, just add that useCallback and useMemo as you go, it is really best practice. Learn the exceptions instead. And follow the original intention of that premature optimization expression and actually stop and analyze (with a profiler) where you are doing too much work when the need arises.