Unexpected uses of useLayoutEffect

Going from animation libraries to galaxy brain state hack

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. When useEffect runs.png

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:

When useLayoutEffect runs.png

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:

When only hooks run.png

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 stable useEvent 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 other useLayoutEffect calls could still reference stale references
  • useSyncExternalStore: The code comment above the useLayoutEffect usage explicitly mentions it needs to saving something for a check that happens in useEffect.

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.