React- useCallback Invalidates Too Often in Practice
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, because a new
onClick function is created on every render of
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
useCallback to ensure
PureHeavyComponent renders only once.
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 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
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
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.
Remember the ‘prehistoric times’ when we used to use “Class Components”?
This works just fine.
this.handleClick is always the same function.
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
thisof the function is not
refto not less the user of the hook mess with
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.
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.