framer / motion

Open source, production-ready animation and gesture library for React
https://framer.com/motion
MIT License
22.71k stars 754 forks source link

[BUG] AnimatePresence exit animations not working with createPortal #2692

Open winkcor opened 1 month ago

winkcor commented 1 month ago

1. Read the FAQs 👇

2. Describe the bug

AnimatePresence only work when it send as children to createPortal it not effected when its as parent of createPortal.

3. IMPORTANT: Provide a CodeSandbox reproduction of the bug

A CodeSandbox

4. Steps to reproduce this is not work and element not render

<AnimatePresence>
  {isOpen &&
    createPortal(
    <>...</>,
      document.body
    )}
</AnimatePresence>

in below code element render but exit animation not working


return(
  createPortal(
          <AnimatePresence>
              {isOpen && <>...</>}
          </AnimatePresence>,
        document.body
      )
);

5. Expected behavior

AnimatePresence and Exit animation work for createPrtal as regular JSX element.

7. Environment details

Chrome 125.0.6422.78
Firefox 126.0.1
Windows 10
june21x commented 1 month ago

in below code element render but exit animation not working

return(
 createPortal(
       <AnimatePresence>
            {isOpen && <>...</>}
       </AnimatePresence>,
      document.body
   )
);

Hi @winkcor, I've tried out this solution, it seems fine to me. CodeSandbox

https://github.com/framer/motion/assets/35992041/ff4c1b0a-ba3d-46ef-8277-3b1b81099527

Environments:

Chrome 125.0.6422.114 (Official Build) (arm64)
macOS Sonoma 14.5
winkcor commented 1 month ago

in below code element render but exit animation not working

return(
 createPortal(
       <AnimatePresence>
            {isOpen && <>...</>}
       </AnimatePresence>,
      document.body
   )
);

Hi @june21x, you are right, this code works, but this is a server component, if we do not set the isOpen condition before createPortal, the component will be rendered on the server side, and the document.body is not defined on the server, and if you look at the server console, you will see the following error:

 ⨯ src\components\modal\index.tsx (51:15) @ document
 ⨯ ReferenceError: document is not defined
    at ModalContainer.Modal (./src/components/modal/index.tsx:81:23)
digest: "3267920918"
  49 |                 }
  50 |             </AnimatePresence>
>51 |             , document.body, this.key);
     |               ^
  52 |
  53 |     };
  54 | }

to fix that we can add if (typeof window === "undefined") return; before createPortal. but I think if we could use the following code its so much better, but with this code, the component is not rendered

<AnimatePresence>
 {isOpen &&
   createPortal(
   <>...</>,
     document.body
   )}
</AnimatePresence>
june21x commented 4 weeks ago

@winkcor I see, I have modified my solution to align with your requirement, by wrapping a Fragment around createPortal(). CodeSandbox

<AnimatePresence>
  {isOpen &&
    <>
      {createPortal(
        <>...</>,
        document.body
      )}
    </>
  }
</AnimatePresence>

Based on the source code here,

https://github.com/framer/motion/blob/c5e5175fd0820135ebe22434cf13e9a11a43b74d/packages/framer-motion/src/components/AnimatePresence/index.tsx#L39

My guess is that <AnimatePresence/> doesn't detect ReactPortal as its children because somehow ReactPortal is not a valid ReactElement(even though ReactPortal does extends ReactElement) when validated with isValidElement(). That's why it's not rendering.

console.log(isValidElement(createPortal(<></>, document.body)})); // false

console.log(isValidElement(<>{createPortal(<></>, document.body)}</>)); // true