How can I safely roll back a Framer Motion animation after a layout schema change using the `animatePresence` feature?
0 reputation · 29 Jul 2022, 15:37 UTC
0 reputation · 29 Jul 2022, 15:37 UTC
When a component tree undergoes a schema change—such as adding, removing, or reordering elements that are animated with Framer Motion—there is a risk that exit animations may be interrupted or that the rollback leaves the UI in an inconsistent state. Using `animatePresence` allows components to animate out before unmounting, but ensuring that the rollback process waits for all exit animations to complete before reverting the schema is not straightforward.
What is the recommended approach to coordinate the schema revert with the completion of exit animations so that the rollback is safe and does not cause visual glitches or memory leaks?
To guarantee that a schema rollback does not leave the UI in a broken state, wrap the entire layout‑changing subtree in <AnimatePresence> and coordinate the rollback with the exit animation’s completion. The key steps are:
key (or layoutId if the same component is reused). This lets Framer Motion detect a transition and trigger the correct exit animation.const [layout, setLayout] = useState('A');) before initiating a change.try block. If the change involves async work, wrap that call in try/catch as well.exitBeforeEnter: Set exitBeforeEnter on <AnimatePresence> so the new layout is not rendered until the old one’s exit animation finishes.onExitComplete callback. If the schema change succeeded, update layout to the new value. If it failed, reset it to the previously stored snapshot.import { AnimatePresence, motion } from "framer-motion";
const LayoutSwitcher = () => {
const [layout, setLayout] = useState('A'); // current layout
const [prevLayout, setPrevLayout] = useState('A'); // snapshot for rollback
const switchTo = async (newLayout) => {
setPrevLayout(layout); // keep a copy
try {
// simulate a schema change that might fail
await performSchemaChange(newLayout);
setLayout(newLayout); // commit only after success
} catch (e) {
// rollback on error
setLayout(prevLayout);
}
};
return (
<AnimatePresence exitBeforeEnter onExitComplete={() => {/* nothing needed here */}}>
{layout === 'A' && (
<motion.div key="A" initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}>Layout A</motion.div>
)}
{layout === 'B' && (
<motion.div key="B" initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}>Layout B</motion.div>
)}
</AnimatePresence>
);
};
exitBeforeEnter does not cause layout thrashing.key props; duplicate keys would break the exit animation.If your change involves heavy async work (e.g., fetching data), make sure the rollback logic is inside the catch block of that async call. Also keep the <AnimatePresence> wrapper in place for future transitions.
onExitComplete fires to ensure the new layout’s nodes are removed.layout state variable reverts to its previous value when a rollback occurs.Use comments to ask for clarification. Post a solution as an answer.
26,525 reputation · 29 Jul 2022, 22:55 UTC
While exitBeforeEnter (or mode="wait" in newer versions) prevents the new layout from rendering until the old one vanishes, it can create a jarring "blank" state during the rollback. To maintain a smoother visual experience, consider using mode="popLayout".
When popLayout is enabled, the exiting element is shifted to position: absolute. This allows the new layout (the rolled-back state) to begin its entry animation immediately in the correct position, while the exiting element fades out on top of it. This avoids the layout "jumping" or collapsing entirely during the transition.
To verify this behavior in your current version of Framer Motion:
AnimatePresence has position: relative.position: absolute during the exit phase.