@Chamion personal blog

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.