I wanted the button to move down to accomodate for a new row added. So I put layout
on the MotionButton
(Which is just a motion(...)
wrapped component).
<div className="grid gap-5">
<AnimatePresence>
{flightDates.map((f, index) => (
<motion.div
key={index}
exit={{
opacity: 0,
y: 10,
}}
initial={{
opacity: 0,
y: 10,
}}
animate={{
opacity: 1,
y: 0,
}}
transition={{
duration: 0.2,
}}
>
<FlightDateRow
key={index}
onChange={(updatedFlight) => {
setFlightDates((prev) => {
const newFlights = [...prev]
newFlights[index] = {
...newFlights[index],
...updatedFlight,
}
return newFlights
})
}}
onDelete={removeFlightDate(index)}
canDelete={flightDates.length > 1}
{...f}
/>
</motion.div>
))}
</AnimatePresence>
<MotionButton
className="w-min pl-2"
variant="secondary"
onClick={addNewFlightDate}
layout
transition={{
duration: 0.2,
}}
>
<PlusMini className="mr-2" />
Flight
</MotionButton>
</div>
I've asked you in the comments if you could add the following debugging statement:
useEffect(() => {
console.log("button mounted");
return () => console.log("button unmounted");
}, []);
Because I suspected you're component was being re-mounted. This would explain the behaviour, since your button would trigger its unmount animation, and then its mount animation.
You confirmed this was indeed the case:
Yes it appears you're right, my button is being re-mounted. When the button is clicked, the console prints out "button mounted" then "button unmounted" and then once again "button mounted".
Since you haven't provided much context in the question I can only guess why it's being re-mounted. Here is a scenario that is probably the most common.
Say though some sort of logic the structure of you output JSX changes from:
<A>
<YourButton />
</A>
to:
<B>
<YourButton />
</B>
This causes <A>
to unmount, which in turn causes all the children of <A>
also unmount. Then <B>
is mounted with all its children. Due to the parent component change of YourButton
, YourButton
will also be unmounted and mounted.
function Demo() {
const [useA, setUseA] = React.useState(true);
const toggleUseA = React.useCallback(() => setUseA(useA => !useA), []);
const Wrapper = useA ? A : B;
return (
<div>
<button onClick={toggleUseA}>use {useA ? "B" : "A"}</button>
<Wrapper><YourButton /></Wrapper>
</div>
);
}
const A = ({ children }) => <div>A{children}</div>;
const B = ({ children }) => <div>B{children}</div>;
function YourButton() {
React.useEffect(() => {
console.log("YourButton mounted");
return () => console.log("YourButton unmounted");
}, []);
return null;
}
ReactDOM.createRoot(document.querySelector("#root")).render(<Demo />);
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<div id="root"></div>
I don't know if this is the exact cause, since there are other reasons why a component might re-mount. But this is in my opinion the most likely cause. It could be be the internals of Framer Motion it could be something else.
I hope this helps you understand why this problem happens and are happy to see you already found a solution to your issue in your own answer.
ps. After looking at your gist it seems like you're doing the exact thing I describe above. See: gist line 49-57
const Comp = asChild ? Slot : 'button' ... return ( <Comp ... > {children} </Comp> )