Search code examples
reactjstypescriptreact-typescriptframer-motion

Why is the button floating up before going down?


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>

Solution

  • 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>
    )