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

Open
dimensi opened this issue Feb 22, 2019 · 44 comments

Comments

@dimensi
Copy link

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

This comment has been minimized.

Copy link
Member

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.

@thysultan

This comment has been minimized.

Copy link

commented Feb 23, 2019

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

@sebmarkbage

This comment has been minimized.

Copy link
Member

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.

@dimensi

This comment has been minimized.

Copy link
Author

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?

@atomiks

This comment has been minimized.

Copy link

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.

@atomiks

This comment has been minimized.

Copy link

commented Feb 24, 2019

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

const useIsomorphicLayoutEffect = isBrowser ? useLayoutEffect : useEffect

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

This comment has been minimized.

Copy link
Member

commented Feb 25, 2019

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

@atomiks

This comment has been minimized.

Copy link

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

@dimensi

This comment has been minimized.

Copy link
Author

commented Mar 5, 2019

Any answers?

@atomiks

This comment has been minimized.

Copy link

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

@gaearon

This comment has been minimized.

Copy link
Member

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.

@gaearon

This comment has been minimized.

Copy link
Member

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} />;
}
@atomiks

This comment has been minimized.

Copy link

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.

@gaearon

This comment has been minimized.

Copy link
Member

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.

@atomiks

This comment has been minimized.

Copy link

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

@gaearon

This comment has been minimized.

Copy link
Member

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.

@eps1lon

This comment has been minimized.

Copy link
Contributor

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.

@gaearon

This comment has been minimized.

Copy link
Member

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.

@eps1lon eps1lon referenced this issue Mar 18, 2019
1 of 1 task complete
@gaearon

This comment has been minimized.

Copy link
Member

commented Mar 19, 2019

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

docs screenshot

@gaearon

This comment has been minimized.

Copy link
Member

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.

@markerikson

This comment has been minimized.

Copy link

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?

@alexreardon

This comment has been minimized.

Copy link

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

@sebmarkbage

This comment has been minimized.

Copy link
Member

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.

@sebmarkbage

This comment has been minimized.

Copy link
Member

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.

@dimensi

This comment has been minimized.

Copy link
Author

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.

@salvoravida

This comment has been minimized.

Copy link

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

@gaearon

This comment has been minimized.

Copy link
Member

commented Mar 20, 2019

@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

This comment has been minimized.

Copy link

commented Mar 20, 2019

@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

This comment has been minimized.

Copy link

commented Mar 20, 2019

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

This comment has been minimized.

Copy link

commented Apr 11, 2019

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

This comment has been minimized.

Copy link

commented Apr 11, 2019

@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

This comment has been minimized.

Copy link

commented Apr 12, 2019

@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

This comment has been minimized.

Copy link

commented Apr 12, 2019

@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

This comment has been minimized.

Copy link

commented Apr 15, 2019

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

This comment has been minimized.

Copy link
Contributor

commented Apr 16, 2019

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

This comment has been minimized.

Copy link

commented Apr 23, 2019

@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

This comment has been minimized.

Copy link

commented Apr 26, 2019

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

This comment has been minimized.

Copy link

commented May 7, 2019

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

This comment has been minimized.

Copy link

commented May 8, 2019

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

cjolowicz added a commit to cjolowicz/muckr-web that referenced this issue May 8, 2019

Use node environment for server-side test
react-redux uses `useLayoutEffect` instead of `useEffect` when `window` is
defined:

https://github.com/reduxjs/react-redux/blob/8605088606403f6909597405c67db91d907db86e/src/components/connectAdvanced.js#L35-L41

By default, Jest defines `window` globally. This can lead to the warning
`useLayoutEffect does nothing on the server` when running the test suite. Change
this behaviour by selecting the `node` test environment instead of the default
`jsdom` environment.

facebook/react#14927 (comment)
@cezarsmpio

This comment has been minimized.

Copy link

commented May 11, 2019

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

This comment has been minimized.

Copy link
Contributor

commented May 25, 2019

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

@codemilli codemilli referenced this issue Jun 21, 2019
@uberska

This comment has been minimized.

Copy link

commented Jul 28, 2019

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!

@alexlee-dev

This comment has been minimized.

Copy link

commented Aug 19, 2019

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
You can’t perform that action at this time.