facebook / react

The library for web and native user interfaces.
https://react.dev
MIT License
229.28k stars 46.96k forks source link

why setState in useLayoutEffect forces useEffects to run before paint? #17334

Closed lsnch closed 4 years ago

lsnch commented 5 years ago

Do you want to request a feature or report a bug?

A bug, but more likely a question.

What is the expected behavior?

I have some pretty intense computations in useEffect. And also some trivial animations in useLayoutEffect.

What I expect of react is to let me paint a page based on what I specified in layout effects. Once it's done it can go on run effects.

What is the current behavior?

What actually happens is if I change state inside layoutEffect, every single effect is run, and not only in this component, but also in every parent up the tree.

If this is expected, why does this happen?

amazzalel-habib commented 5 years ago

If I understand your point well, I think you could use requestAnimationFrame twice in your layoutEffect to set the new state, so that the effect don't happen til the next paint.

lsnch commented 5 years ago

That's unlikely since the animation is somewhere deep inside Material UI. I have a heavy tree with effects, and a few MUI components change the way my render unfolds.

Let me know if it's a bug and I'll provide a reproduction.

bvaughn commented 5 years ago

Layout effects are for things that need to happen before paint (e.g. measuring the position of a tooltip). If such an effect needs to re-render (e.g. to re-position the tooltip) then it must happen synchronously, so the user only sees a single paint (e.g. never sees the tooltip in the wrong position to begin with).

As part of this second, synchronous render, React also flushes the remaining/pending effects (including the passive ones- which would otherwise have not been flushed until a later frame).

lsnch commented 5 years ago

Layout effects are for things that need to happen before paint (e.g. measuring the position of a tooltip). If such an effect needs to re-render (e.g. to re-position the tooltip) then it must happen synchronously, so the user only sees a single paint (e.g. never sees the tooltip in the wrong position to begin with).

Couldn't agree more.

As part of this second, synchronous render, React also flushes the remaining/pending effects (including the passive ones- which would otherwise have not been flushed until a later frame).

But useEffect is designed exactly for this? To do stuff soon, but not before you have picture? React docs mention this everywhere:

https://reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects

Also, don’t forget that React defers running useEffect until after the browser has painted

https://reactjs.org/docs/hooks-overview.html#effect-hook

When you call useEffect, you’re telling React to run your “effect” function after flushing changes to the DOM

https://reactjs.org/docs/hooks-reference.html#useeffect

The function passed to useEffect will run after the render is committed to the screen.

https://reactjs.org/docs/hooks-reference.html#timing-of-effects

the function passed to useEffect fires after layout and paint, during a deferred event.

It not some obscure typo in a dark corner of the docs. Rather, it's the defining feature of this hook and it's repeated over and over. I find this behavior inconsistent and contradicting the documentation.

bvaughn commented 5 years ago

As part of this second, synchronous render, React also flushes the remaining/pending effects (including the passive ones- which would otherwise have not been flushed until a later frame).

That's why I mentioned the part that's bolded. useEffect is called after paint in the optimal case.

Triggering a re-render (sometimes also called a "cascading render") is a de-opt case, since it requires React to immediately do more rendering work (which delays the paint). This is important for certain cases, like the one I mentioned above (positioning a tooltip), but it's best to avoid if possible because of reasons like we're discussing.

bvaughn commented 4 years ago

Cleaning up issues and closing this one because the question seems to be answered. 👍

SrBrahma commented 2 years ago

Layout effects are for things that need to happen before paint (e.g. measuring the position of a tooltip). If such an effect needs to re-render (e.g. to re-position the tooltip) then it must happen synchronously, so the user only sees a single paint (e.g. never sees the tooltip in the wrong position to begin with).

As part of this second, synchronous render, React also flushes the remaining/pending effects (including the passive ones- which would otherwise have not been flushed until a later frame).

Hi @bvaughn. Sorry for comenting on such an old post. Found this issue via Google and hopefully you can help me.

I am the author of https://github.com/SrBrahma/react-native-shadow-2. On its current implementation, it first applies the shadow in a relative way (that may have a 1pixel gap/overlap), then gets the children's size with onLayout, setState the exact children's size, and on the next render the exact size is applied to have a perfect SVG shadow sizing and positioning (now it has a specific bug with iOS but that's another issue).

Would be possible to use useLayoutEffect somehow to apply the exact component size on the first render, without painting the intermediary state? onLayout doc says it is called before the render, after the calculation.

bvaughn commented 2 years ago

Would be possible to use useLayoutEffect somehow to apply the exact component size on the first render, without painting the intermediary state?

The browser won't paint if layout effect is used to schedule a re-render that changes the output of the previous render. The browser will have to calculate layout (for you to read measurements) but no paint will occur until after the 2nd render.

OliverJAsh commented 3 months ago

As part of this second, synchronous render, React also flushes the remaining/pending effects (including the passive ones- which would otherwise have not been flushed until a later frame).

This behaviour surprised me too. I don't see it mentioned anywhere in the React documentation, so it was never part of my mental model.

Could we add this to the documentation?

eps1lon commented 3 months ago

@OliverJAsh Good point. I think this deserves its own entry in https://react.dev/reference/react/useLayoutEffect#caveats. Can you send a PR and ping me in it?

OliverJAsh commented 3 months ago

Sure! https://github.com/reactjs/react.dev/pull/7096