Description
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:
- Synchronously before paint
- In "parallel" in reads/writes/reads etc
- After all child components in the tree have run
useLayoutEffect
- including those entering the tree (currently these don't mount until after otheruseLayoutEffect
s 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 read
s from across the tree, then all write
s, then all read
s 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.