Why is the button floating up before going down?

50 Views Asked by At

enter image description here

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>
2

There are 2 best solutions below

0
3limin4t0r On BEST ANSWER

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>
)
0
Stephen Horton On

I ended up having to wrap my button in a <motion.div layout></motion.div> instead of converting the button to a MotionButton. My button component would cause a remount to occur when clicking the button. So relying on an outer div to manage position within the layout fixed it.