Skip to content

Feature request: A useLayoutEffect with read/write batching across a tree #20068

Open
@mattgperry

Description

@mattgperry

Today, useLayoutEffect can be used for synchronous read/writes across the DOM.

useLayoutEffect(() => {
  // Write
  ref.current.style.transform = ""

  // Read
  const box = ref.current.getBoundingClientRect()
})

For a single instance of a single component, this works well. But if this code is repeated or reused anywhere in the tree, we trigger layout thrashing. The severity of the layout thrashing scales linearly with the number of hooks/components featuring either this code or code like it.

Hooks and components are designed to be composable, yet it's this trivial to write one that isn't.

Instead, what I'd like is a batched version of useLayoutEffect that provides read and write callbacks. These schedule callbacks that will be called:

  1. Synchronously before paint
  2. In "parallel" in reads/writes/reads etc
  3. After all child components in the tree have run useLayoutEffect - including those entering the tree (currently these don't mount until after other useLayoutEffects have been called)

It could look like this, though I'm more interested in the above specs than actual API:

useBatchedLayoutEffect((read, write) => {
  write(() =>  {
    ref.current.style.transform = ""
  })

  read(() => {
    const box = ref.current.getBoundingClientRect()
  })
})

Then, adhering the 3 specifications above, these callbacks are executed in order, so all reads from across the tree, then all writes, then all reads etc. There is no upper limit for the number of permitted ping-ponged reads/writes IMO as the amount of layout thrashing you could possibly suffer will never be worse than the single hungriest hook. In my experience I've never needed more than a read/write/read/write.

Measurement accuracy

In the given example, we're measuring a component after first resetting its transform because we want to snapshot its actual bounding box without any transforms applied. But if this component is nested in itself, so both a parent and child are performing the same type of animation, we want to unset all the transforms before measuring any of the elements, otherwise the resulting measurements will be incorrect.

What about requestAnimationFrame?

Theoretically it could be possible to implement this ourselves in userland by creating a batcher that executes on the next available frame. Sadly this isn't possible in practise. For whatever browser-internal reason it is necessary to run these reads and writes synchronously to prevent flashes of incorrectly-styled components.

If you play with this very simple layout animation implementation by clicking on the red box you'll see it suffers no flashes:

https://codesandbox.io/s/broken-star-cycfz?file=/src/App.js:528-537

But if you uncomment the wrapping requestAnimationFrame within the useLayoutEffect you'll see it does randomly flash with the undesired styles.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions