Consistent referential identity
React hooks rely on identity equality to determine whether deps have changed or not. A function will gain a new referential identity each time it is regenerated. If a function is defined inline in a component, using it in deps will always have those deps change even if they are functionally equivalent.
Using callbacks
It's admittedly convenient to write a function inline.
return <CustomButton onClick={() => setEnabled(true)}>Enable</CustomButton>;
The inline lambda onClick
will have CustomButton
rerender every time even if it has memoization.
Not only that, CustomButton
will recalculate any of its callbacks that have onClick
as a dependency.
If any of its children use those callbacks, they too rerender and so on.
Let's prevent the cascade of memoization failures with a useCallback
.
const handleClick = useCallback(() => setEnabled(true), [setEnabled]);
return <CustomButton onClick={handleClick}>Enable</CustomButton>;
Now the referential identity of onClick
will be consistent.
Callbacks are special props in that their referential identity carries information that cannot be inferred by the child component. Objects and arrays can be compared based on their properties and elements respectively. There are no equivalence functions to compare functions other than identity. Therefore a child component must assume a changed identity in a callback prop means memoized values cannot be used.
Consequences of inconsistency
Consider a hypothetical custom select component.
It returns some JSX using provided props but it's a heavy chunk of JSX to make it all work.
React.memo
can memoize it to only rerender when props change.
However, memoization becomes useless if just one callback, like an onChange
prop, has an inconsistent referential identity.
Writing memoization usually involves changing parent components as well to ensure consistent referential identities in props. The parent needs to change because it had made an assumption about the children: that they wouldn't suffer considerable performance costs. When it comes to performance, components are coupled when you fail to ensure consistent referential identities. Sometimes props with inconsistent referential identities originate from far away ancestor components or a context, not just the immediate parents, thus coupling the whole spaghetti together.
Not directly optimizing
In the past I've had senior developers point out that memoizing one-liner functions is computationally more expensive than regenerating them each time. While well intentioned, the complaint misses the point of a consistent referential identity.
Ensuring consistent referential identities is about clean code, not optimizing. It clarifies the prop interface between components; an unchanged callback will have the same identity. Consistent referential identities afford you the flexibility to address optimization issues locally at a component level. Without consistency you'd have to make changes in multiple components to optimize one.