Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

useLayoutEffect in ssr #14927

Closed
dimensi opened this issue Feb 22, 2019 · 60 comments
Closed

useLayoutEffect in ssr #14927

dimensi opened this issue Feb 22, 2019 · 60 comments

Comments

@dimensi
Copy link

@dimensi dimensi commented Feb 22, 2019

Hi, I do not understand the situation with this hook a bit. I use this hook to perform the animation synchronously with the state update, if I use useEffect, then I have jumps in the animation, because the animation library does not have time to start. Also, the documentation states that useLayoutEffect runs on the same phase as componentDidMount (that is, on the client side), and here my server issues complaints to me about my code. Why is that?

https://codesandbox.io/s/oo47nj9mk9

Originally posted by @dimensi in #14596 (comment)

@sebmarkbage
Copy link
Member

@sebmarkbage sebmarkbage commented Feb 23, 2019

It’s there to force you to think about whether you truly need it to be useLayoutEffect (uncommon) or if you’re ok with it being useEffect (common).

useLayoutEffect exists to give you strong guarantees about having the ability to adjust layout last minute without painting between. However we cannot guarantee that if you’re server rendering this component. It has to be resilient to work without this guarantee.

Loading

@thysultan
Copy link

@thysultan thysultan commented Feb 23, 2019

Related thread on some of the effects of this in practice: Twitter Link.

Loading

@sebmarkbage
Copy link
Member

@sebmarkbage sebmarkbage commented Feb 23, 2019

I can imagine some theoretical cases where it would be legit to want to useLayoutEffect only after some state has switched.

However in practice I haven’t seen this yet. All cases I’ve seen so far have been slightly broken when you SSR if you think about it.

It would be good to show actual examples if you have them.

Loading

@dimensi
Copy link
Author

@dimensi dimensi commented Feb 23, 2019

@sebmarkbage
https://codesandbox.io/s/7y96862n2q
try this.

Still, the documentation is directly written that the useLayoutEffect phase is the same as componentDidMount / Update and these lifecycle methods are never executed on ssr, so why do the server complain to me about useLayoutEffect?

Loading

@atomiks
Copy link

@atomiks atomiks commented Feb 24, 2019

I'm integrating a 3rd party lib that mutates DOM elements in the tree, and it needs to happen before first paint to prevent a "jitter" (when the tooltip updates its content while showing, the position needs to be updated synchronously or there will be a flicker). This warning occurs during SSR, but as mentioned above, I don't see why if it's equivalent to the cDM/cDU lifecycles that never did.

The library is purely client-side, so it doesn't have any effects on the server markup. Tooltips don't get displayed on page load until hydration phase. So it can't be "broken" in that sense.

Loading

@atomiks
Copy link

@atomiks atomiks commented Feb 24, 2019

Maybe this could work or would it break the rules of hooks?

const useIsomorphicLayoutEffect = isBrowser ? useLayoutEffect : useEffect

function Comp() {
  useIsomorphicLayoutEffect(() => {
    // ...
  })
}

Loading

@sebmarkbage
Copy link
Member

@sebmarkbage sebmarkbage commented Feb 25, 2019

@atomiks How does that component work with server rendering? Isn’t there a small jitter?

Loading

@atomiks
Copy link

@atomiks atomiks commented Feb 25, 2019

@sebmarkbage it's a tooltip library that creates a separate element that only exists after mounting. The element it's attached to gets rendered normally (server or client), but the tooltip doesn't appear until after hydration.

Details The content is rendered in a portal. When the content gets updated by React, the `.set()` method should be be called synchronously for the position to be updated. Because it uses position: absolute and doesn't exist in the normal flow of the document, changes to the size of the tooltip causes it to be repositioned in the wrong place, so the `translate` transform needs to be updated immediately before painting, or there will be a jitter.

A ResizeObserver could also work, but not enough support yet.

Demo with useEffect

jitters
  • Happens once on first load, twice on second load
  • When it reaches the boundary, it doesn't happen because the position doesn't need to be updated

No jitters with useLayoutEffect

Loading

@dimensi
Copy link
Author

@dimensi dimensi commented Mar 5, 2019

Any answers?

Loading

@atomiks
Copy link

@atomiks atomiks commented Mar 5, 2019

@dimensi I am using the trick in #14927 (comment) and it seems to be working fine for my component library. I had to release a patch just to get around it.

I don't see why there's even a warning at all, what's the point? We need to use hacks to get around it anyway because it is required in many legitimate cases, but still want to use it in SSR without problems (no-op behavior is fine anyway)...

Loading

@gaearon
Copy link
Member

@gaearon gaearon commented Mar 5, 2019

what's the point

The point of the warning is to warn you that your component will behave weirdly before hydration. You may disagree or ignore the recommendation in specific cases if you want but it's pointing out a legitimate problem.

Loading

@gaearon
Copy link
Member

@gaearon gaearon commented Mar 5, 2019

If something only exists after hydration then the canonical solution to it is to render a fallback view first. And then flip a flag inside useEffect that would show and activate your plugin.

function useIsMounted() {
  let [mounted, setMounted] = useState(false);
  useEffect(() => {
    setMounted(true);
  }, []);
  return mounted;
}

function Foo() {
  let ref = useRef();
  let isMounted = useIsMounted();
  useEffect(() => {
    if (isMounted) {
      $.myJqueryPlugin(ref.current);
    }
  }, [isMounted]);
  if (!isMounted) {
    return <FallbackView />;
  }
  return <div ref={ref} />;
}

Loading

@atomiks
Copy link

@atomiks atomiks commented Mar 5, 2019

It's not a problem in this case though, as mentioned above. And I still need useLayoutEffect on updates, as mentioned above also. There's no way to get around it..

Basically, I think warnings that can't be turned off when there are use cases for it should not exist. Just make it prominent on the docs which effect hook to use instead.

Loading

@gaearon
Copy link
Member

@gaearon gaearon commented Mar 5, 2019

I think warnings that can't be turned off when there are use cases for it should not exist

I generally agree with that. That’s why the issue is still open. If there are legit use cases we’ll want to either adjust the warning or write documentation for it. Currently there are higher priority issues so we’re looking into this first. Since you can work around. We’ll get back to this and fix it after those.

Loading

@atomiks
Copy link

@atomiks atomiks commented Mar 6, 2019

One thing that's a bit worrying with useEffect is that people are using it by default for a lot of layout-related things, unknowingly. Even things that don't seem layout-related, like window.scrollTo. I noticed in different threads where people are unexpectedly creating jittery UIs as a side effect now. And the worst part is it can be reproduced <50% of the time based on when the browser decides to paint when rendering, so it can be harder to catch.

I think the differences between the hooks should be highlighted better (when to use them, etc.)

I almost feel like useLayoutEffect should have been called useEffect, and useEffect called useDeferredEffect. I think the sync behavior is preferred, because non-jittery UIs is better than not blocking 1 frame(?)

Loading

@gaearon
Copy link
Member

@gaearon gaearon commented Mar 14, 2019

The thing with jitter is that once you see it, you can fix it. With a layout effect. Or by calculating the correct initial state early (if you jitter due to a setState). It’s more annoying to first experience it — but it’s intentional that sometimes you’d see it and that would tell you “okay, here I need a layout effect instead”. This is feature working as designed.

However the majority of code people put into DidMount/DidUpdate actually doesn’t need to be sync. So it’s a worse perf default which becomes death by a thousand cuts in a larger trees.

In the past all code was sync. This has bad consequences for perf. Now it’s usually async, and when it causes jitter you just opt back into sync for that specific case. I think this makes sense. The naming is intentionally shorter for useEffect because we want you to try it first. And only replace it it actually causes a problem. (For most application code it doesn’t.)

I agree it can be annoying if you see a warning which can’t be fixed. Collecting more use cases that seem legit here would help.

For example I’m not sure the scroll one is legit if you use SSR. Scroll does represent a “layout effect” to me because you want it to happen together with layout instead or flickering. But what about SSR? You don’t want to start interacting with a page, scroll it, and then hydration scrolls it back. This is bad UX. So maybe your initial scroll logic should be outside of React components altogether, and could be inline in HTML. Then you’d be sure not to “scroll to top” too late. What do you think? More detailed use cases like this would help.

Loading

@eps1lon
Copy link
Collaborator

@eps1lon eps1lon commented Mar 14, 2019

This should probably be mentioned in the docs: https://reactjs.org/docs/hooks-reference.html#uselayouteffect

That section makes it pretty clear to me that I should use useLayoutEffect if I migrate from class components. It doesn't mention the warning for server side rendering though.

Happy to submit a docs PR if I'm not the only one that finds the current documentation confusing.

Loading

@gaearon
Copy link
Member

@gaearon gaearon commented Mar 14, 2019

Sure, to be honest I want to rewrite that whole page because a lot of people find different parts about it confusing.

Loading

@gaearon
Copy link
Member

@gaearon gaearon commented Mar 19, 2019

I tweaked the docs to explain why this warning exists: https://reactjs.org/docs/hooks-reference.html#uselayouteffect

docs screenshot

Loading

@gaearon
Copy link
Member

@gaearon gaearon commented Mar 19, 2019

@dimensi

Regarding your example in #14927 (comment). You didn't provide any details at all. What are the steps I need to do? What is the expected behavior? What is the actual behavior? Can you make a screenshot of the issue?

It's very hard to help when you provide a couple hundred lines with no explanation of what problem you're running into. I understand the "abstract" problem (useLayoutEffect helps you in some way) but it would be so much easier to think about this if you provided the actual reproduction steps.

Loading

@markerikson
Copy link

@markerikson markerikson commented Mar 20, 2019

It's starting to look like React-Redux's v7 implementation of connect may need to call useLayoutEffect() internally to avoid a timing issue.

The repro on this is somewhere between "complex" and "convoluted", but it involves a mixture of a sync setState() in a parent and a dispatched Redux action combined with unstable_batchedUpdates(), all in the same tick.

I'm not sure I can even come up with a good TL;DR: for this one. @alexreardon has repro sandboxes linked in reduxjs/react-redux#1177 (comment) , and I wrote up a step-by-step description of what's going on in reduxjs/react-redux#1177 (comment) .

useLayoutEffect() seems to resolve the issue, because it guarantees that a ref I'm using for coordination will be written to before any other logic (like a Redux store subscription) would possibly execute. (Specifically, the store subscription callback always needs access to the absolute latest props passed to the wrapper component when it runs mapState and computes the new child props.)

But, given that React-Redux is widely used for SSR, I don't want all our users to be seeing this warning printed all the time.

As an additional edge case: I know I've seen cases where ReactDOM.renderToString() is being used on the client. The specific example that comes to mind was some kind of a Google Maps wrapper that needed to put HTML strings into an <InfoWindow> component, and so renderToString() was being used to allow generating that HTML content with standard React components. Seems like this would also warn in that scenario?

Loading

@alexreardon
Copy link

@alexreardon alexreardon commented Mar 20, 2019

For timing reasons, react-beautiful-dnd leans heavily on useLayoutEffect. We need to tightly control user input event flows. My understanding would be that none of these functions would run in an SSR environment. It is a surprise to me that this would log a warning.

react-beautiful-dnd supports SSR and having these warnings would be lame for consumers

Loading

@sebmarkbage
Copy link
Member

@sebmarkbage sebmarkbage commented Mar 20, 2019

For the react-beautiful-dnd case, if that was a leaf component, I'd say only conditionally render the component after initial render and render a fallback first since nothing will actually work until it's hydrated anyway. In fact, this is a good example for using a lazy component to load the richer functionality in lazily instead of paying for that during initial render. Since apparently it's fine to have a gap where this functionality doesn't work (which happens during SSR).

However you probably don't want to swap out the tree when this happens and you can't really swap out the component because it's a custom Hook.

This sounds like a good use case for "progressively enhancing hooks" which is a thing we've been thinking about. It's basically a way to let you load new Hooks into a component while it's already running. These new Hooks can then useLayoutEffect because they're loaded late and never during SSR. This guarantees that even in the client case, this speeds up initial rendering and still forces the component to deal with the gap between initial render and progressive enhancement.

Loading

@sebmarkbage
Copy link
Member

@sebmarkbage sebmarkbage commented Mar 20, 2019

For the Redux case I believe this is related to #15122 (comment)

useEffect is basically just a concurrent mode-light and if you can't deal with the gap between rendering completing and commit phase, you're probably going to have even bigger problems when each render can also yield and gets dispatches injected in the middle.

Loading

@dimensi
Copy link
Author

@dimensi dimensi commented Mar 20, 2019

@gaearon
The essence of my example is that I have 2 modes of animation work there: through useEffect and via useLayoutEffect.
If you select the useEffect mode (which is the default) and switch between slides, you can see how the text "jumps" and because animejs does not have time to apply all the styles when rendering the component. If you switch to useLayoutEffect, then there are no such problems, the animation works smoothly and the text does not "jump".
With this example, I want to show how important useLayoutEffect for animation and that it does not affect the first render of the component.
And no, I don't want to use any dirty tricks to bypass the warning that creates react when rendering my component.
Because it is important for me that the content that is in the components was available immediately, and not after the initialization of react on the client. If I use tricks with stubs or conditions, then I will generally lose the sense of using ssr.

Loading

@salvoravida
Copy link

@salvoravida salvoravida commented Mar 20, 2019

@gaearon @sebmarkbage

as reduxjs/react-redux#1177 (comment)

i suggest IMHO, that you explain in hooks docs,
that ref.current updates should not be done inside useEffect (as it may be deferred)

ref.current should be update as soon as there is a new value, like "last props received"
and of course it is fine to save it in the render phase (always last props)

As i can see, this seems to be a common useRef error.

That's all
Regards

Loading

@aleclarson
Copy link
Contributor

@aleclarson aleclarson commented Nov 10, 2019

The easiest solution for library authors is to use this package.

Loading

@necolas
Copy link
Contributor

@necolas necolas commented Jan 9, 2020

Closing this as answered.

Facebook essentially uses this strategy internally, and solving for the case where an effect condition can only happen due to an update is something we will look to support in the future as we introduce new APIs.

Loading

@necolas necolas closed this Jan 9, 2020
@gaearon
Copy link
Member

@gaearon gaearon commented Jan 9, 2020

To clarify, the link above is an escape hatch and should only be used when you understand the consequences. Before you use this, read this part of the docs:

If you use server rendering, keep in mind that neither useLayoutEffect nor useEffect can run until the JavaScript is downloaded. This is why React warns when a server-rendered component contains useLayoutEffect. To fix this, either move that logic to useEffect (if it isn’t necessary for the first render), or delay showing that component until after the client renders (if the HTML looks broken until useLayoutEffect runs).

To exclude a component that needs layout effects from the server-rendered HTML, render it conditionally with showChild && <Child /> and defer showing it with useEffect(() => { setShowChild(true); }, []). This way, the UI doesn’t appear broken before hydration.

If you absolutely need to work around it, sure, #14927 (comment) works. Although I'd say useIsomorphicLayoutEffect is an extremely confusing name for this pattern.

At FB, this Hook is called useLayoutEffect_SAFE_FOR_SSR which calls out that you understand the consequences. Maybe you'll want to name yours something similar.

Loading

@markerikson
Copy link

@markerikson markerikson commented Jan 9, 2020

Is there a definitive implementation of the isBrowser check that can be officially recommended?

For React-Redux, we've had to keep making this more complicated, and run into issues due to folks running tests with Jest+JSDOM, as well as handling React Native.

Issues for reference:

Frankly, this warning continues to be a pain for us to deal with.

Loading

@StevieJayCee
Copy link

@StevieJayCee StevieJayCee commented Nov 27, 2020

Giving this another bump, as useIsomorphicLayoutEffect gets confused by Jest+JSDOM as @markerikson pointed out. We either have lots of test log noise withthis error warning or have a separate maintenance overhead with separated test classes for SSR, annotated with the alternative jest node environment.

Loading

@iDVB
Copy link

@iDVB iDVB commented Mar 31, 2021

@gaearon Regarding the tweak to the docs you made. I've been reading up a fair bit on the useLayoutEffect warning and SSR conversation. I think I fully understand the "what's happening" but still completely confused as to the why have a warning for only useLayoutEffect

From those docs...

"keep in mind that neither useLayoutEffect nor useEffect can run until the JavaScript is downloaded. This is why React warns when a server-rendered component contains useLayoutEffect. "

Why then are we providing a warning only for useLayoutEffect? Both will cause renders on the client and not on the server.

The most I can reason, is that the react team have identified that many people are using useLayoutEffect when it would be better to use useEffect. This seems like something better to leave to docs and education rather than forced warnings.

For us, we explicitly use useLayoutEffect in order to setup complex timelined animations.
Using useEffect causes blinking between routes and on mount, as would lazy loading those animations.
Using useLayoutEffect has served us perfectly for years by rendering the animations, element locations etc before things render to the browser, so there is no blink. So the solution for us to avoid these warnings is to implement isomorphicLayoutEffect like switches.

It seems there a plenty of valid use cases for useLayoutEffect to warrant finding a more subtle / off-build way to advise developers of when best to use it. No?

Loading

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Linked pull requests

Successfully merging a pull request may close this issue.