I'm adding some React modules to an existing website. To do so, I'm mounting my React components via React Portals.
In short, while my components do mount as expected, the animations are not triggered. Eg, they remain stuck with an opacity
of 0
. I would expect these components to transition from opacity
0
to 1
on mount, then from 0
to 1
on exit as per the initial
animate
and exit
props on each child within <AnimatePresence>
.
A contrived but working example showing the bug can be found on CodeSandbox here.
Code from the CodeSandbox also shown below:
index.html
<!DOCTYPE html>
<html lang="en">
<head>
...
</head>
<body>
<div id="react-root"></div>
<h1>Static Content</h1>
<button id="start-portal">Show Modal</button>
<div id="portal"></div>
</body>
</html>
Context.js
import React from "react";
const INITIAL_STATE = {
activeItem: -1
};
const Context = React.createContext(null);
const ContextProvider = ({ children }) => {
const [store, setStore] = React.useState(INITIAL_STATE);
React.useEffect(() => {
const startEl = document.getElementById("start-portal");
startEl.addEventListener("click", () => setActiveItem(0));
return () => {
startEl.removeEventListener("click", () => setActiveItem(0));
};
}, []);
const setActiveItem = (activeItem) => {
console.log("SETTING ACTIVE ITEM TO " + activeItem);
setStore((prevState) => {
return {
...prevState,
activeItem
};
});
};
return (
<Context.Provider
value={{
...store,
setActiveItem
}}
>
{children}
</Context.Provider>
);
};
function useContext() {
return React.useContext(Context);
}
export { ContextProvider, useContext };
index.js
import React from "react";
import ReactDOM from "react-dom";
import Modal from "./Modal";
import { ContextProvider } from "./Context";
import "./styles.css";
const REACT_ROOT = document.getElementById("react-root");
const PORTAL_ROOT = document.getElementById("portal");
const PortalModal = () => {
if (!PORTAL_ROOT) return null;
return ReactDOM.createPortal(<Modal />, PORTAL_ROOT);
};
const App = () => (
<ContextProvider>
<PortalModal />
</ContextProvider>
);
ReactDOM.render(<App />, REACT_ROOT);
Modal.js
import React from "react";
import { AnimatePresence, m } from "framer-motion";
import { useContext } from "./Context";
const Modal = () => {
const { activeItem, setActiveItem } = useContext();
return (
<AnimatePresence exitBeforeEnter>
{activeItem === 0 && (
<m.div
key={"step-1"}
className={"step"}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
<h2>STEP 1</h2>
<button onClick={() => setActiveItem(-1)}>CLOSE</button>
<button onClick={() => setActiveItem(1)}>NEXT</button>
</m.div>
)}
{activeItem === 1 && (
<m.div
key={"step-2"}
className={"step"}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
<h2>STEP 2</h2>
<button onClick={() => setActiveItem(-1)}>CLOSE</button>
<button onClick={() => setActiveItem(0)}>PREV</button>
</m.div>
)}
</AnimatePresence>
);
};
export default Modal;
You can switch from m
to motion
(in the import and components) and everything works as expected.
m
isn't a 1:1 alias for motion
, it's a slimmed-down version.
If you want to use animations with the m
component you'll need to import an additional "feature package" like domAnimation
or domMax
.
More info here: Framer.com: Reduce bundle size