React- useCallback Invalidates Too Often in Practice

What is issue #14099 in React’s repo and how it affects you?

Vitali Zaidman
Welldone Software
3 min readMay 25, 2020

--

Why do we need useCallback in the first place?

In the official documentation of useCallback it says:

“This is useful when passing callbacks to optimized child components that rely on reference equality to prevent unnecessary renders”

In the following example, PureHeavyComponent would re-render every single time that the Parent component is re-rendered although PureHeavyComponent is pure because previous props.onClick !== new props.onClick, because a new onClick function is created on every render of Parent.

A naive code where a pure heavy component is re-rendered on every re-render of its parent.

Since PureHeavyComponent is “heavy” we want it to re-render as less as possible. This is why we made it pure. But if we pass a new prop to it (onClick) on every render, we don’t achieve this goal.

This is where our useCallback comes into play

We wrap onClick with useCallback to ensure PureHeavyComponent renders only once.

Here the pure heavy component is not re-rendered on any render of its parent after the first one.

useCallback caches (“memoizes”) the first function that was passed to it on the first render of Parent and always passes the same one to PureHeavyComponent.

Since PureHeavyComponent is pure, and since all of its props are equal this way, it doesn’t re-render anymore.

The Issue With useCallback

If any of the deps of useCallback change, handleClick is “invalidated” which means, it would no longer use the memoized value, but the new one that’s passed to it.

Consider the following code:

If Parent re-renders for any reason other then clicking on “increase count”, handleClick won’t be invalidated and everything works as expected.

But, whenever you click on the “increase count” button, since count changes, handleClick will be invalidated and PureHeavyComponent would re-render.

In this case since this render is “heavy”, it might cause a lag and a performance issue since it would slow down the application’s response time to a click.

This is exactly what the issue useCallback() invalidates too often in practice #14099 is all about.

Workarounds

Class Component

Remember the ‘prehistoric times’ when we used to use “Class Components”?

Using class components

This works just fine. this.handleClick is always the same function.

useEventCallback

This hook and its drawback are also discussed in the official React docs.

useEventCallback does something clever. It saves the last function passed to it on useRef and exposes a memoized function that calls the saved function with all the relevant args.

But, this pattern might cause problems in the upcoming version of React with concurrent mode, so it is not recommended to be used unless you understand very well what’s the dangers involved in using it.

By the way the weird and rarely used syntax (0, ref.current)(args) here is using the comma operator to ensure the this of the function is not ref to not less the user of the hook mess with ref by mistake.

Here’s a sandbox demonstrating the issue and the useEventCallback workaround.

A Possible Future Bug Fix

The React Core Team might improve the hook to always return the same function that would call the latest function that was passed to it. This way, wrapping a function with useCallback would never make pure components that use it re-render because of it.

It’s even possible to remove the second argument (deps) from the hook altogether and just keep on calling the latest function the hook received.

I believe this kind of solution would make the hook much more powerful, safe and easy to understand.

Summary

While there’s indeed an issue with useCallback, in most cases it works just fine. Recognizing the edge cases where it would be invalidated too often and using a workaround might improve your application’s performance in these edge cases.

Related articles you might find useful:

Why Did You Render v4 Released!

React Element’s “Parent” vs “Rendered By”

--

--