Motion: [BUG] AnimatePresence : children in the tree can be stuck in exit/initial variant if reentry is triggered before exiting is done

Created on 27 Oct 2019  路  5Comments  路  Source: framer/motion

Hello,

When using AnimatePresence with children down the tree, and triggering the "animate" variant before the "exit" one finishes, if the component that houses the child motion component doesn't re-render on its own due to the change, the motion component will stay stuck in the initial/exit variant and stay there until the component is properly unmounted (meaning letting the exit animation run til the end).

My setup is a bit complicated to reproduce, but I managed to reproduce the bug pretty easily by using React.memo to prevent the rerendering.

See here for the CodeSandbox. Just click on the "Go" button once, let the animation finish, then click "Go" again and then "Go" a third time before the exit animation finishes. The "Child Memo" component will then stay stuck in the middle whereas it should have gone all the way to the left.

Interestingly, as you can see in my reproduction, despite not re-rendering just like the "Child Memo" component, the "Root memo" component, as a direct child of AnimatePresence, does animate properly.

Also, I am currently trying to migrate from Pose to Framer Motion, and the exact same setup worked fine with PoseGroup. This is preventing me from going forward with it (which is a bit of a shame seeing how much better Motion's API is!).

If it helps, this is the actual problem I'm facing:

(the animation works the first two times and then I am too fast for the third time and it gets stuck. It's triggered by scrolling so it's easy enough to reproduce in my application)

bug

Most helpful comment

This needs to be solved, same issue here.

All 5 comments

After some deep dive:

When "enter" is triggered during an exit animation:

1) AnimatePresence re-renders Parent with no exitProps in context.
2) Parent's render function is called.
3) MotionComponent's render function is triggered.
4) In MotionComponent's render function, useValueAnimationControls is called.
5) useValueAnimationControls executes a side effect during render, deleting all children. This side effect removes Child from Parent's subscribers. Doing this during render is not very relevant, but I wanted to point it out because it'll most likely break with react in concurrent mode anyways.
6) Because Child is memo'ed and because there are no updates to the context that parent calculates, its render function is not called. Thus useValueAnimationControls is not called, which was supposed to subscribe to Parent's controls.
6) Exit functionality's lifecycle hook is triggered, which runs Parent's controls.start(animate).
7) Because Parent doesn't have any subscribed children anymore at this point, Child animation doesn't play.

I was curious about why this case is working without AnimatePresence/Exit and here's my findings:

1) For AnimatePresence/Enter, it's obvious. Everything needs to rerender, which triggers Child's render. So this works.
2) For everything that causes Parent to re-render by changing its animation related properties, it will cause MotionContext to change, which will trigger a render for Child. So this works.
3) For everything else that uses the imperative API of AnimationControls, the behaviour is very hard to predict with all the moving parts and possible race conditions.

Having the same issue, my implementation looks exactly like the one from the api examples section: https://codesandbox.io/s/framer-motion-x-react-router-n7qhp

Any ideas on how to mitigate the issue?

I've had a similar issue. The animation got stuck, when the router pushed back to the old route, before the transition to the new one was finished.

for example imagine the following:

/private-route     authorized == true;
                   *browsers back button pressed*

/logout            authorized = false;
                   *browsers forward button pressed*

/private-route     authorized == false;
                   *redirect back, b/c user is not authorized*

/logout            *route-transition wasn't finished*
                   *route-transition get's stuck on exit*

ezgif com-optimize

I could solve this issue by changing:

initial={{ x: 0, opacity: 0, zIndex: 0 }}
animate={{ zIndex: 0, opacity: 1 }}
exit={{ x: '-20px', zIndex: 1, opacity: 0 }}

to:

initial={{ x: 0, opacity: 0, zIndex: 0 }}
animate={{ x: 0, zIndex: 0, opacity: 1 }}
exit={{ x: '-20px', zIndex: 1, opacity: 0 }}

It seems the motion component does not reset/animate to the initial state automatically.
And I had to tell the motion component to animate x back to 0 no matter what.

Maybe this helps you guys with your specific issue.

This can't be fixed unless Framer changes its "convenience" API. Framer needs to

1) stop breaking React's (or side-effect free rendering philoshophy's) contract about not doing side effects during render
2) stop assuming that renders will trigger synchronously, one child after another.

Above points are a problem because Framer tried to make the API as convenient as possible for API consumers.

This needs to be solved, same issue here.

Was this page helpful?
0 / 5 - 0 ratings