-
Notifications
You must be signed in to change notification settings - Fork 809
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
Make AnimatePresence CM-safe #826
Conversation
a012051
to
0f86796
Compare
We can only update The key to making things CM-safe is to defer any render-related updates to effects and compute render output based on the same values that were previously committed (and props) - we can't manipulate refs in render, at all costs. We need to prepare new values for refs though (so they can be committed in an effect). What mutable refs we had previously here?
Let's take a closer look to each one: exitingThis was used to re-insert exiting nodes into the returned elements and to keep track of when we need to force rerender to clear "ghost" DOM nodes in a single batch. To improve the situation around this I've gone with a route of recomputing this for each render - to compute what's currently exiting we need 2 things: incoming children and currently rendered ones, exiting children can be fully derived from those 2 things. By having this computed in the closure itself we can still track if all exiting children have been exited. We only need to remove them from a mutable presentChildrenThis is a tricky one - this represents what is being rendered on the screen but yet we use a mutable value for this 😱 It operates on the assumption that it's safe to return different result from render based on the same props. This sounds terrible. But maybe in this very case it's not? What we care about is a synchronization to the final state of things - in the end we only want to have the same children in the DOM tree that we got in the props. Even if a child exits in the middle of rerendering it seems OK to remove it from a mutable This is quite convoluted and I had to rethink this using some more visuals - you might find it helpful as well. In here we have 3 components: A B C and first we exit, later B. Exiting components are marked with
Q: Can Framer Motion handle gracefully a "ghost" DOM node that reenters? Even though a "ghost" element gets removed from allChildrenThis basically stays untouched - only we update this in an effect now. I expect this might not work properly with React's capabilities to commit higher priority updates first even if a lower priority render has been already computed, rebasing of those updates etc. My mind kinda melts when I think about those - but there is also a lot of unknown regarding those capabilities because they are marketed even less than time-slicing. There is also a chance that this would actually work with those capabilities - I'm not sure. After all, we actually don't want to render those "ghost" children at all - we only try to optimize stuff here by batching final removal or piggybacking on React's rerenders in the middle of the flow. |
Hey there, Matt wanted me to chime in because I've been looking into this sort of thing recently. So I've been working on a version of AnimatePresence for use at FB and since we are all-in on concurrent mode I needed to ensure its compatibility with it. The biggest rule when ensuring concurrent mode compatibility is to never modify mutable data (i.e. refs) in render, since we can't guarantee that any one render will eventually be committed and we can't guarantee the renders will happen in a certain order. A component's render — with a given props and state — should (for the most part) always return the same thing. If there are side-effects that alter mutable data, they should be only done in events or effects. The approach I ended up taking has a lot of the same logic that already exists in FM's AnimatePresence implementation but the biggest difference is that instead of refs to store The actual way to accomplish this is to implement the I hope that this explanation helps and if anything is still unclear I'd be happy to answer any questions you have. |
This has been done here - refs are only read in the render, which is also frowned upon but it has been done with extra care (happy to be proven that I've missed some scenarios in which this still ain't safe, this is very tricky to get right so I fully expect that I could have missed something).
This was an approach used by Motion's predecessor - React Pose. So I'm in a sense familiar with it - I just haven't wanted to refactor too much here, especially given what you mention: this pattern with hooks is awkward. We could consider it here - probably even refactoring to a class component like you have done, but things get complicated with this approach because of the fact that Motion tries to only remove exiting components from the "state" when all exiting components actually have exited. So it needs to maintain some mutable copy of that "state" to do that. It also has to maintain its state across rerenders (or just be able to recompute what has been mutated in subsequent renders). This happens here: motion/src/components/AnimatePresence/index.tsx Lines 213 to 229 in 0f86796
Do u know of any good way to test this stuff? We need to be able to test this with great control over rerenders, handlers, commits - using |
This is just an experiment right now - but Jest's tests pass, so I'm optimistic 😅
The logic within
AnimatePresence
here is very delicate and my changes affect the timing of refs updates so I need to rethink all scenarios to check if this code is functionally identical to the previous version. If you spot something that might not work properly - let me know.