-
Notifications
You must be signed in to change notification settings - Fork 875
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
[Presence] React 18 – Fix animation frame sync flicker #1344
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I do feel like it's a better fix too, I've left a few comments inline.
// With React 18 concurrency this update is applied | ||
// a frame after the animation ends, creating a flash of visible content. | ||
// By manually flushing we ensure they sync within a frame, removing the flash. | ||
ReactDOM.flushSync(() => send('ANIMATION_END')); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interesting…
I'm no unclear as to when flushSync
should be used or not.
As far as I understand, we use flushSync
to opt-out of automatic batching.
But what's not clear to me here, is that if it was automatically batching and that was the issue, why would it be delayed by a frame? Unless it is batching 2 state updates "later", and opting out results in applying the first one at the correct time for us?
In which case that would kinda make sense.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But what's not clear to me here, is that if it was automatically batching and that was the issue, why would it be delayed by a frame?
This isn't entirely clear to me either, my current thought is that React is pushing this to the next tick as part of the batching mechanism, I saw this eluded to in the wg post:
React doesn’t render the result of the first setState synchronously—the render occurs during the next browser tick. So the render hasn’t happened yet
some code may depend on reading something from the DOM immediately after a state change. For those use cases, you can use ReactDOM.flushSync() to opt out of batching
Which I think is why we're seeing the state change trail the native event, here's a reduced example:
https://codesandbox.io/s/elated-snow-x36ypm?file=/src/App.js
I just remembered this post, which suggests forcing sync as a solution, though that was in the context of useEffect
, I suppose the mechanics are similar here but with the events
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is what I'm confused about though with that reduced example.
Once you isolate just the bad case (the red box), there's only really 1 setState right? So where's the batching? There's no batching in this case, or are we saying that React delays it always regardless of whether there are more than 1 updates?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
are we saying that React delays it always regardless of whether there are more than 1 updates?
Yes, in a way. I think it's more a case of concurrency fundamentally changing its relationship with browser rendering and how paints are scheduled.
Here's another example that would suggest the same:
Try resizing the grey box using the handle:
https://codesandbox.io/s/amazing-montalcini-j6lz46?file=/src/App.js:894-899
Notice how the native resize is painting ahead of state.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Man yeah definitely seems to be the case, it's pretty clear here.
It does make me think we probably need to do it in other places too then, as we use things like resize observer too…
The worse part in all that really, the part that bothers me is that it seems like there's no way to tell where it should be applied until you see an issue really…
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I hear you, though I do think we’ll begin to see best practices emerge over time through guidance from the core team and community (as we did with state, FCs, hooks etc). I’d like to understand the underlying mechanics better though, right now it’s not particularly intuitive when to reach for certain tools.
Something that crossed my mind is where we explicitly opt out of batching using setTimeout
when programatically focusing, nothing has come up yet though so we're probably ok. We might need to think about how to test in 17 if we decide to make changes in any spicy areas.
@@ -21,7 +21,8 @@ | |||
"@radix-ui/react-use-layout-effect": "workspace:*" | |||
}, | |||
"peerDependencies": { | |||
"react": "^16.8 || ^17.0 || ^18.0" | |||
"react": "^16.8 || ^17.0 || ^18.0", | |||
"react-dom": "^16.8 || ^17.0 || ^18.0" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because of this, we will need to track all of the dependents of Presence
and their dependends subsequently and add that peer-dependency too.
Sometimes I wonder if it would be easier to always add them both as peer-dep anyway haha, would there be any harm considering that they are meant to be used with the DOM anyway?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point!
Sometimes I wonder if it would be easier to always add them both as peer-dep anyway haha, would there be any harm considering that they are meant to be used with the DOM anyway?
Practically speaking I don't think it'd be a problem, technically inaccurate in terms of the dependency contracts but maybe that's ok in this case? at least we'd never forget in the future.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah exactly, I'm mostly worried that it's hard to track through multiple levels of dependencies, if it was just flat, it might be ok, but it becomes a bit harder when there's more than 1 level.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But I guess let's see how it evolves, it may end up that they mostly all have it anyway for one reason or another (basically if they or any of their deps uses Portal, or Presence now)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Approving for the code change, but still needs to deps changes.
super educational following along here 👀 i just wondered if this would work in React 16.8 tho and my quick hacky try yielded a "no":
something to look into perhaps? 😬 |
Yikes!! |
Interesting, wasn’t aware of this limitation in 16 but worth keeping in mind! I know that's contrived but Given that syncing state in https://codesandbox.io/s/react-16-8-0-forked-t6fhlc?file=/src/components/App.js Thanks for mentioning, underscores this point:
|
oh right, I missed the |
ah my bad, i saw that react 17+ doesn't error with my sandbox, interestingly. glad it wasn't a tricky one! |
Yeah in your sandbox in 17 it doesn't error but it has a warning instead, seems they changed that in 17. |
* [Presence] flushSync dom with react * Update stories * Provide comment * Update dependent peerDeps
closes #1074
related #1292 #1125
The introduction of batched updates across all handlers in 18 concurrent mode introduces a frame sync issue where the state transition (
unmount
) is communicated a frame after animation has completed.Currently we are recommending
animation-fill-mode: forwards
to cover this gap. While it does work, it creates other problems.I'm still getting my head around the internal mechanics of react scheduling so feedback here would be very welcome, but as it stands I feel that this change is a superior option vs the css we are recommending.
forwards
recommendation from demos if accepted?Before:
flicker.mp4
After:
no-flicker.mp4