React- useCallback Invalidates Too Often in Practice
What is issue #14099 in React’s repo and how it affects you?
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 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.
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”?
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 thethis
of the function is notref
to not less the user of the hook mess withref
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.