Closed dimensi closed 4 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.
Related thread on some of the effects of this in practice: Twitter Link.
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.
@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?
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.
Maybe this could work or would it break the rules of hooks?
const useIsomorphicLayoutEffect = isBrowser ? useLayoutEffect : useEffect
function Comp() {
useIsomorphicLayoutEffect(() => {
// ...
})
}
@atomiks How does that component work with server rendering? Isn’t there a small jitter?
@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.
Any answers?
@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)...
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.
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} />;
}
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.
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.
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(?)
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.
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.
Sure, to be honest I want to rewrite that whole page because a lot of people find different parts about it confusing.
I tweaked the docs to explain why this warning exists: https://reactjs.org/docs/hooks-reference.html#uselayouteffect
@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.
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?
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
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.
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.
@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.
@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
@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.)
@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.
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);
}
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?
@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 : 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.
@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.
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!
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?
@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
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?
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 ?
@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.
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.
One of the more popular patterns to avoid invalidating useCallback
too often will also trigger this warning
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!
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
andrender
in the same file.mount
requires a DOM, so we used jest's jsdom testEnvironment. When we upgraded React, the test that callsrender
started showing the warning aboutuseLayoutEffect
. react-redux was choosinguseLayoutEffect
in the child connected component because it's in a browser-like environment due to jest's jsdom testEnvironment. Since enzyme'srender
usesReactDOMServer
, 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 torender
. 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
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:
requestAnimationFrame
or useLayoutEffect
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.
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
}
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.
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.
@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.
@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.
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