Unexpected uses of useLayoutEffect
Going from animation libraries to galaxy brain state hack
Photo by Anna Kolosyuk on Unsplash
The docs describe useLayoutEffect as a DOM targeted escape hatch for visual updates to the UI. Why then is a hook like this showing up in:
- Compatibility shims for
useSyncExternalStore
- Reference implementation for the proposed
useEvent
hook addition? - In the react-redux useSelector implementation (before being moved over to useSyncExternalStore).
My answer is that there seems to be an emergent use case of useLayoutEffect
, one that takes advantage of when it happens to run the React update process. I'd describe it as a lifecycle hook for other hooks. To understand why, let's look at the original use case for useLayoutEffect
.
Original Superpower: Run Before Browser Paint
Here is the ordering for when React updates the DOM and when effects run.
React explicitly yields the main thread to let the browser paint the frame. This is a departure from the old component lifecycle componentDidMount
but it is in most cases what you want and more performant for synchronization effects that don't immediately have visual updates.
When you do have visual updates, however, doing them in the useEffect
can cause flicker in the UI. useLayoutEffect
offers an escape hatch for this:
This is the original use case and animation libraries like Framer Motion make use of it for performant animations.
New Superpower: Run before useEffect
There is another important property of useLayoutEffect
, however: It runs before useEffect
:
Why is this important? It's for the use case of setting up non-rendering infrastructure across components. Effects and event handlers need to operate on the latest references, for example, but updating them in the render body is impure and useEffect
may be too late. Although hooks have explicit execution order within a component, there is no guarantee in React that a particular component will update before any other.
I did not write any of these examples but here is my stab at why useLayoutEffect
was used in:
- Old
useSelector
implementation: Essentially 'initializes' the selector by updating references for event handlers/effects, creating the subscription and evaluating it again on the update. useEvent
compatibility shim: The updates references that the stableuseEvent
points to before the possibility of event handlers or effects using stale references. It is pointed out that this is not a complete solution, though, because otheruseLayoutEffect
calls could still reference stale referencesuseSyncExternalStore
: The code comment above theuseLayoutEffect
usage explicitly mentions it needs to saving something for a check that happens inuseEffect
.
I believe this usage of useLayoutEffect
is indicative of the need for a new injection point in the React update process, which is being addressed via things like useSyncExternalStore
and useEvent
. I believe such usage will fade away as React develops appropriate abstractions but it's interesting to think back on how emergent practices in the community get turned into new features.