facebook / react

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

useLayoutEffect in ssr #14927

Closed dimensi closed 4 years ago

dimensi commented 5 years ago

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 https://github.com/facebook/react/pull/14596#issuecomment-466023638

sebmarkbage commented 5 years ago

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.

thysultan commented 5 years ago

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

sebmarkbage commented 5 years ago

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.

dimensi commented 5 years ago

@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?

atomiks commented 5 years ago

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.

atomiks commented 5 years ago

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

const useIsomorphicLayoutEffect = isBrowser ? useLayoutEffect : useEffect

function Comp() {
  useIsomorphicLayoutEffect(() => {
    // ...
  })
}
sebmarkbage commented 5 years ago

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

atomiks commented 5 years ago

@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`
dimensi commented 5 years ago

Any answers?

atomiks commented 5 years ago

@dimensi I am using the trick in https://github.com/facebook/react/issues/14927#issuecomment-466815232 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)...

gaearon commented 5 years ago

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.

gaearon commented 5 years ago

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} />;
}
atomiks commented 5 years ago

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.

gaearon commented 5 years ago

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.

atomiks commented 5 years ago

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(?)

gaearon commented 5 years ago

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.

eps1lon commented 5 years ago

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.

gaearon commented 5 years ago

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

gaearon commented 5 years ago

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

docs screenshot
gaearon commented 5 years ago

@dimensi

Regarding your example in https://github.com/facebook/react/issues/14927#issuecomment-466703167. 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.

markerikson commented 5 years ago

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 https://github.com/reduxjs/react-redux/issues/1177#issuecomment-474638250 , and I wrote up a step-by-step description of what's going on in https://github.com/reduxjs/react-redux/issues/1177#issuecomment-474671590 .

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?

alexreardon commented 5 years ago

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

alexreardon commented 5 years ago
sebmarkbage commented 5 years ago

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.

sebmarkbage commented 5 years ago

For the Redux case I believe this is related to https://github.com/facebook/react/pull/15122#issuecomment-473591116

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.

dimensi commented 5 years ago

@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.

salvoravida commented 5 years ago

@gaearon @sebmarkbage

as https://github.com/reduxjs/react-redux/issues/1177#issuecomment-474784122

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

gaearon commented 5 years ago

@dimensi Seems like your case is also the use case for “progressively enhancing” Hooks. (We don’t yet have an official solution to it but it’s something we’ve wanted to add.)

markerikson commented 5 years ago

@sebmarkbage : yeah, I think I'm inclined to agree on both points.

This was one of the primary reasons why I switched us over to state propagation via context in React-Redux v6, but.... well, perf and hook updates, as we've talked about before :( I'd happily stick with our current v6 approach if context worked the way we needed it to, but it doesn't sound like that's going to happen at this point.

v7 with direct subscriptions and use of unstable_batchedUpdates is working right now, but I agree that Bad Things (TM) are likely to happen with Concurrent Mode. We'll deal with that whenever CM is finally ready. I gotta come up with something that solves the issues our users are facing right now.

Now, I will note that React-Redux v5 did roughly this bit of logic in componentDidUpdate, which is the equivalent of useLayoutEffect(), so it would be reasonable for us to continue to keep up that same behavior here.

brandonburkett commented 5 years ago

My SSR logs :(

For attaching event listeners, per the docs, it seems I should use useLayoutEffect. This use case is for main navigation (I'm converting from a class component) and auto-closing the menu if the user clicks outside the navigation menu component. For simplicity, I wouldn't want to do something odd just to get around the warning message on SSR (as I didn't need too with the class based component).

EX

  componentDidMount() {
    document.addEventListener('mousedown', this.handleClickOutside);
  }

  componentWillUnmount() {
    document.removeEventListener('mousedown', this.handleClickOutside);
  }
VicJer commented 5 years ago

It is kinda annoying that warning as it spams the hell out of my tests even thought they are passing most of them come from react and react-redux connect method. Anyone came across it? Any way I can get rid of them?

markerikson commented 5 years ago

@VicJer : v7 actually tries to fall back to useEffect in an SSR scenario specifically to avoid this. Are you still seeing those warnings in a test environment?

VicJer commented 5 years ago

@VicJer : v7 actually tries to fall back to useEffect in an SSR scenario specifically to avoid this. Are you still seeing those warnings in a test environment?

@markerikson I am on 7.0.1 yeah I can still see it.

atomiks commented 5 years ago

@VicJer I'm guessing you're adding global.window = someValue to the Node environment then. I ended up adding && typeof document !== 'undefined' for more safety, but if you're also declaring a global document then that also won't help. Usually document gets namespaced under global.window.document if browser env is being simulated though so it should work most of the time.

VicJer commented 5 years ago

I can't see it being added anywhere if I am honest. But I think I narrowed it down and it seems to me like it's Enzyme's render method. Thanks for all your help it pointed me in the right direction. Much appreciated!

eps1lon commented 5 years ago

What about

React.useLayoutEffect(() => {
  if (autoFocus) {
    listItemRef.current.focus();
  }
}, [autoFocus]);

? focus() could change visuals of listItemRef.current. Valid use case to trick React into thinking that this is a useEffect on the server or not?

alexreardon commented 5 years ago

@sebmarkbage right now react-beautiful-dnd cannot move to a "progressively enhancing hooks" pattern as a Droppable currently sets up context for a Draggable - something a hook cannot do

menberg commented 5 years ago

I'm using Overmind for state management. While this library is made for the browser, I also can use it in a node environment. As the library utilises useLayoutEffect for some client side optimisations, I get the warning: useLayoutEffect does nothing on the server. Can I mute the warning somehow as my Terminal gets spammed?

mandarzope commented 5 years ago

When ever I pass initialState ={product:{....}} to createStore I get following error on server side render. If I make initialState = {} error goes away.

Warning: useLayoutEffect does nothing on the server, because its effect cannot be encoded into the server renderer's output format. This will lead to a mismatch between the initial, non-hydrated UI and the intended UI. To avoid this, useLayoutEffect should only be used in components that render exclusively on the client. See https://fb.me/react-uselayouteffect-ssr for common fixes. in ConnectFunction in ConnectFunction in div in Product in Context.Provider in ConnectFunction in ConnectFunction in Context.Provider in Context.Consumer in Route in Fragment in App in Context.Provider in Router in StaticRouter in Context.Provider in Provider

It is because connect()() function is using useLayoutEffect internally ?

cjolowicz commented 5 years ago

@mandarzope react-redux uses useLayoutEffect instead of useEffect when window is defined:

See react-redux/src/components/connectAdvanced.js, lines 35 to 41:

// React currently throws a warning when using useLayoutEffect on the server.
// To get around it, we can conditionally useEffect on the server (no-op) and
// useLayoutEffect in the browser. We need useLayoutEffect because we want
// `connect` to perform sync updates to a ref to save the latest props after
// a render is actually committed to the DOM.
const useIsomorphicLayoutEffect =
  typeof window !== 'undefined' ? useLayoutEffect : useEffect

By default, Jest defines window globally. This can lead to the useLayoutEffect does nothing on the server warning when running the test suite.

You can change this behaviour by selecting the node test environment instead of the default jsdom environment. Try adding a @jest-environment docblock to the very top of your test file:

/**
 * @jest-environment node
 */

Or, to select the node environment globally, use this in your package.json:

{
  "name": "my-project",
  "jest": {
    "testEnvironment": "node"
  }
}

The warning about useLayoutEffect should disappear because window is no longer defined during test execution.

cezarsmpio commented 5 years ago

I couldn't find any solution to use useEffect instead of useLayoutEffect in Safari when injecting CSS code on the fly.

That's my hook, it works fine for all browsers when using useLayoutEffect:

const cachedStyles = [];

function useInjectStyle(rule) {
  useLayoutEffect(function() {
    if (cachedStyles.indexOf(rule) >= 0) return;

    cachedStyles.push(rule);

    const styleElement = document.createElement('style');
    styleElement.appendChild(document.createTextNode(''));

    document.head.appendChild(styleElement);

    styleElement.sheet.insertRule(rule, styleElement.sheet.cssRules.length);
  }, []);
}

If I try to use CSS animations, it doesn't work in Safari using useEffect but it does work if I use useLayoutEffect.

const keyframesRule = `@keyframes awesomeAnimationName { from { background-position: 0 center; } to { background-position: -200% center; } }`;
useInjectStyle(keyframesRule);

It simply doesn't add the animation only in Safari, all other browsers are fine with useEffect apparently.

eps1lon commented 5 years ago

One of the more popular patterns to avoid invalidating useCallback too often will also trigger this warning

uberska commented 5 years ago

We ran into this warning when we were using jest and enzyme to test a component with a child connected component. We had tests for the component that called enzyme's mount and render in the same file. mount requires a DOM, so we used jest's jsdom testEnvironment. When we upgraded React, the test that calls render started showing the warning about useLayoutEffect. react-redux was choosing useLayoutEffect in the child connected component because it's in a browser-like environment due to jest's jsdom testEnvironment. Since enzyme's render uses ReactDOMServer, we got the useLayoutEffect warning.

The solution is straightforward once you understand what's going on. I split the tests into two files. One file has the jsdom testEnvironment pragma and contains all the calls to mount. The other file has the node testEnvironment pragma and contains all the calls to render. If there are any ideas for keeping the tests in one file, let me know. I kind of like having a one-to-one mapping from component to component test file, but it's obviously not required nor critical. Thanks!

actuallyReallyAlex commented 5 years ago

We ran into this warning when we were using jest and enzyme to test a component with a child connected component. We had tests for the component that called enzyme's mount and render in the same file. mount requires a DOM, so we used jest's jsdom testEnvironment. When we upgraded React, the test that calls render started showing the warning about useLayoutEffect. react-redux was choosing useLayoutEffect in the child connected component because it's in a browser-like environment due to jest's jsdom testEnvironment. Since enzyme's render uses ReactDOMServer, we got the useLayoutEffect warning.

The solution is straightforward once you understand what's going on. I split the tests into two files. One file has the jsdom testEnvironment pragma and contains all the calls to mount. The other file has the node testEnvironment pragma and contains all the calls to render. If there are any ideas for keeping the tests in one file, let me know. I kind of like having a one-to-one mapping from component to component test file, but it's obviously not required nor critical. Thanks!

Can you reference the 2 test files here for an example of how you solved this? @uberska

EECOLOR commented 5 years ago

We do a lot of visual appealing websites with transitions all over the place. To make sure we do not enter hard to debug problems we have the following rules:

In some cases we need to set a style property on the DOM, for example overflow: hidden when we have a modal or full screen menu. Our effect would look like something this:

const isMounted = useRenderIfMounted() // returns true if mounted
useLayoutEffect(
  () => {
    if (isMounted) {
      // perform writes
    }
    () => { /* undo writes */ }
  }.
  [isMounted]
)

As you can see there is a guard to make sure it does not run on the server.

JoostKiens commented 5 years ago

A quick solution is a component which renders another component after mounting. The second component executes the effect and returns null.

function UseLayoutEffect({ effect, dependencies }) {
  const isMounted = useRenderIfMounted() // returns true if mounted
  return isMounted && <ActualEffect {...{ effect, dependencies }} />
}

function ActualEffect({ effect, dependencies }) {
  React.useLayoutEffect(effect, dependencies)
  return null
}
EECOLOR commented 5 years ago

The workaround is usable, but it nullifies the eslinting rules that help tremendously.

Since our default mode is server-side rendering and then adding universal stuff for the interactive parts, it would really help if we could somehow disable the warning. I think the best solution would actually be a React.useMountedLayoutEffect(...). There is no point in laying things out when nothing has been mounted.

EECOLOR commented 5 years ago

Just thinking out loud. I think it is easier to this:

const isBrowser = typeof window !== 'undefined'
if (isBrowser) React.useLayoutEffect(...)

While the rules state a hook should not be used within an if statement, that rule is only in place to make sure any memory slots assigned do not get mixed up. This if statement does not cause problems because the isBrowser switch will never change within an environment.

React has the boolean canUseDOM, so theoretically we could use that. It would be ideal if the if statement was in React.useLayoutEffect itself. This however does not work if you take other forms of rendering into account (non-dom rendering).

If I was the library author I would explore the idea of asking the renderer (is that Fiber?) if it supports useLayoutEffect. The default renderer could then use the canUseDOM while the native renderer (or canvas renderer) could use another mechanism.

eps1lon commented 5 years ago

@EECOLOR It warns not depending on DOM detection but if a certain API is used (react-dom/server). You can run ReactDOM.renderToStaticMarkup in the browser which will trigger this warning. No environment sniffing can help you mitigate this.

EECOLOR commented 5 years ago

@eps1lon I understand what you are saying.

I don't think that rendering a component both on the server and on the client is uncommon. Using useEffect does not result in a warning because it will only be executed after the paint (which does not happen on the server).

It seems to be well known that useEffect has no effect when a component is rendered on the server. This should be the same for useLayoutEffect, there is no way use cases for using it (writing to the DOM or other global browser API's) would succeed in a server environment.

I am hoping that useLayoutEffect can get to a similar state as useEffect: that it will not be executed when there is no need for it. At the moment this is up to the user of the library and the warning helps the user of the library to not forget fixing the problem. I think we can do better relatively easy, it would move the burden from the user (developer) to an if-statement.