motiondivision / motion

A modern animation library for React and JavaScript
https://motion.dev
MIT License
24.59k stars 824 forks source link

[BUG] AnimatePresence doesn't work inside AnimatePresence #746

Open em opened 4 years ago

em commented 4 years ago

2. Describe the bug

When AnimatePresence is used inside another AnimatePresence, all transitions happen simultaneously.

3. IMPORTANT: Provide a CodeSandbox reproduction of the bug

https://codesandbox.io/s/framer-motion-2-animating-shared-layouts-forked-jxxsc?file=/src/App.js

4. Steps to reproduce

Steps to reproduce the behavior:

  1. Put AnimatePresence inside another AnimatePresence

5. Expected behavior

  1. The child AnimatePresence transitions from initial -> animate, after the parent finishes animating.
  2. The parent AnimatePresence transitions from animate -> exit , after the child finishes exiting.

Note: The FAQ mentions "AnimatePresence" must be outside of the control conditions. I don't think this applies... because AnimatePresence can easily be aware of its own context and is itself fundamentally a way of overriding control conditions. The parent AnimatePresence should know that it contains another AnimatePresence and delay exiting. And the child AnimatePresence should know its parent has not finished animating in and delay its transition.

Sorry if this should be a "feature request" and not a "bug". It just seems like a bug to the user, but I can see how it's a special case to handle in the implementation. And AnimatePresence just isn't very useful if this isn't handled because otherwise it doesn't correlate to the mental model of conditional rendering in React. Components can render other components... and those components should be able to use AnimatePresence themselves independently without caring if they happened themselves to be rendered by another AnimatePresence.

55Cancri commented 4 years ago

I am having a similar issue where the exit prop of an AnimatePresence's grandchildren are not triggering if those grandchildren are conditionally rendered. Adding the nested AnimatePresence to surround those conditionally rendered components should theoretically solve the issue but it does not.

mattgperry commented 4 years ago

In the attached example, We have code that is essentially:

<AnimatePresence>
    { shouldRender && <motion.div
      initial={{ opacity: 0 }}
      animate={{ opacity: 1 }}
      exit={{ opacity: 0 }}
    >
      <AnimatePresence>
        <motion.div
          initial={{ opacity: 0 }}
          animate={{ opacity: 1 }}
          exit={{ opacity: 0 }}

There is nothing here that should orchestrate a sequential animation. It would be unexpected for these animations to not run simultaneously.

As it is, the child doesn't actually animate at all as it isn't being removed from its AnimatePresence parent. I can potentially see a feature request for parent exits to "pass through" child AnimatePresence components but I'd argue that that contravenes this:

those components should be able to use AnimatePresence themselves independently without caring if they happened themselves to be rendered by another AnimatePresence.

There is nothing in this bug report that suggests a child wouldn't be able to render its own AnimatePresence. The child AnimatePresence isn't being used in this example. So if AnimatePresence did pass through exits from above, we could well end up with quite "sticky" AnimatePresence components where (for instance) route-level AnimatePresence triggers exit animations on a nested (for example) tabbed interface. So the API actually become less composable.

mattgperry commented 4 years ago

@55Cancri have you got a reproduction of this?

55Cancri commented 4 years ago

@InventingWithMonster Yes, here is a codesandbox demonstrating the exact behavior I am observing:

https://codesandbox.io/s/animatepresence-conditionally-rendered-grandchildren-7d3vd?file=/src/index.tsx

em commented 4 years ago

@InventingWithMonster I can appreciate it seems unexpected to you. I think you might be a little biased though because you're building it and there's a consistency in the implementation.

Now - "what's expected" is something we can identify by just asking people. And even if you're right and it is unexpected by most people, it still is worth asking if some cohort of users does expect something different. So then it can just be seen as a matter of supporting both and the most expected behavior determining what the sensible default is.

I can only speak for myself. When I think about AnimatePresence - I see it as: I can write a component, give it enter & exit animation, and every time I render it - I will see that enter animation. Every time it's removed - I will see the exit animation. To get that result, I think the only way is to make AnimatePresence context-aware and delay when it is inside another AnimatePresence, and visa-versa on exit with the ancestor waiting for descendent AnimatePresence.

In isolated examples that only use one AnimatePresence in a sandbox, my mental model is compatible with the implementation. Only when AnimatePresence exists inside AnimatePresence does that break, and I have to really think hard about how AnimatePresence internally works to find a workaround to get the behavior I expected, and need to accomplish my goals.

It doesn't seem such a workaround is actually possible with AnimatePresence AFAICT - the only way to get multiple sequential enter and exit animations in a chain would be to use variants and show/hide things that are all rendered all the time but not visible. That approach doesn't work with a lot of other stuff that adds and removes components, and even just CSS layout in general, so it becomes a really leaky abstraction to design everything else around the animations.

In the end I find myself asking what the point of AnimatePresence is - if I can't really depend on it for showing and hiding things, and will in the end have to rethink my IA and refactor with variants that just make things visible or not with opacity or height but always present in the DOM. On the other hand if AnimatePresence did work the way I expect it would be incredibly powerful and would make so many complex animations much simpler.

Side note - if you search the github issues for "AnimatePresence", I'm not entirely sure but it seems like a lot of other issues people are describing are just variations of this root cause.

ghost commented 4 years ago

I just ran into the same issue as @55Cancri's reproduction link. I'm using react-router and my routes are inside AnimatePresence. Exit animations using AnimatePresence inside of a route component don't start.

55Cancri commented 4 years ago

@InventingWithMonster @joaopaulobdac Should I open this as a separate issue rather than keep it under this feature label? I have not been able to figure out a way to get the exit animations of grandchildren to animate. Do you see anything that I am doing wrong in my code @InventingWithMonster ?

ghost commented 3 years ago

@mattgperry Sorry but could you give us an update on this if possible? I try not to demand things of open source mantainers but I also haven't been able to fix this still

mrbjjackson commented 3 years ago

Just in case this helps anyone: I was having trouble with an AnimatePrescence that was a child of another AnimatePrescence component. I thought there might be a bug or limitation causing this but I just found out it's because my motion elements didn't have "key" props. Not the first time that one's caught me out.

mattgperry commented 2 years ago

Just revisted this. I simply don't agree orchestration is expected here, nor does it scale throughout a tree.

I can see an argument that exit animations should fire in grandchildren, but this is also potentially not the case - think a list with 100s of items. It would make sense for one of them to animate out when removed from the list. But not at all reasonable to animate them all out when an ancestor is removed from its own AnimatePresence.

If and when I get to this I think it's most reasonable to keep the current behaviour but also add a new prop, something like propagateExit or the like, where you can opt-in to exits in parents propagating throughout children.

joeyvanlierop commented 1 year ago

@mattgperry I am currently running into this "issue", and I would like to contribute to a solution. Do you have any suggestions or pointers on where I should start before I dive into the deep end?

adamkui commented 1 year ago

For anyone else still looking at this solution: I was able to fix this issues, by rendering the child component having AnimatePresence, wrapped by a useMemo. So it updated the component inside on selection in my case, and did nothing on exiting together with the parent - which is exactly what I expected. Hope this helps someone :)

jakeleboeuf commented 3 months ago

I'm currently running into the same issue here. Something like a propagateExit would 100% solve my use case too 🙏.