import * as React from 'react'; import { useContext, useRef, cloneElement, Children, isValidElement } from 'react'; import { useForceUpdate } from '../../utils/use-force-update.mjs'; import { useIsMounted } from '../../utils/use-is-mounted.mjs'; import { PresenceChild } from './PresenceChild.mjs'; import { LayoutGroupContext } from '../../context/LayoutGroupContext.mjs'; import { useIsomorphicLayoutEffect } from '../../utils/use-isomorphic-effect.mjs'; import { useUnmountEffect } from '../../utils/use-unmount-effect.mjs'; import { invariant } from '../../utils/errors.mjs'; const getChildKey = (child) => child.key || ""; function updateChildLookup(children, allChildren) { children.forEach((child) => { const key = getChildKey(child); allChildren.set(key, child); }); } function onlyElements(children) { const filtered = []; // We use forEach here instead of map as map mutates the component key by preprending `.$` Children.forEach(children, (child) => { if (isValidElement(child)) filtered.push(child); }); return filtered; } /** * `AnimatePresence` enables the animation of components that have been removed from the tree. * * When adding/removing more than a single child, every child **must** be given a unique `key` prop. * * Any `motion` components that have an `exit` property defined will animate out when removed from * the tree. * * ```jsx * import { motion, AnimatePresence } from 'framer-motion' * * export const Items = ({ items }) => ( * * {items.map(item => ( * * ))} * * ) * ``` * * You can sequence exit animations throughout a tree using variants. * * If a child contains multiple `motion` components with `exit` props, it will only unmount the child * once all `motion` components have finished animating out. Likewise, any components using * `usePresence` all need to call `safeToRemove`. * * @public */ const AnimatePresence = ({ children, custom, initial = true, onExitComplete, exitBeforeEnter, presenceAffectsLayout = true, mode = "sync", }) => { invariant(!exitBeforeEnter, "Replace exitBeforeEnter with mode='wait'"); // We want to force a re-render once all exiting animations have finished. We // either use a local forceRender function, or one from a parent context if it exists. const forceRender = useContext(LayoutGroupContext).forceRender || useForceUpdate()[0]; const isMounted = useIsMounted(); // Filter out any children that aren't ReactElements. We can only track ReactElements with a props.key const filteredChildren = onlyElements(children); let childrenToRender = filteredChildren; const exitingChildren = useRef(new Map()).current; // Keep a living record of the children we're actually rendering so we // can diff to figure out which are entering and exiting const presentChildren = useRef(childrenToRender); // A lookup table to quickly reference components by key const allChildren = useRef(new Map()).current; // If this is the initial component render, just deal with logic surrounding whether // we play onMount animations or not. const isInitialRender = useRef(true); useIsomorphicLayoutEffect(() => { isInitialRender.current = false; updateChildLookup(filteredChildren, allChildren); presentChildren.current = childrenToRender; }); useUnmountEffect(() => { isInitialRender.current = true; allChildren.clear(); exitingChildren.clear(); }); if (isInitialRender.current) { return (React.createElement(React.Fragment, null, childrenToRender.map((child) => (React.createElement(PresenceChild, { key: getChildKey(child), isPresent: true, initial: initial ? undefined : false, presenceAffectsLayout: presenceAffectsLayout, mode: mode }, child))))); } // If this is a subsequent render, deal with entering and exiting children childrenToRender = [...childrenToRender]; // Diff the keys of the currently-present and target children to update our // exiting list. const presentKeys = presentChildren.current.map(getChildKey); const targetKeys = filteredChildren.map(getChildKey); // Diff the present children with our target children and mark those that are exiting const numPresent = presentKeys.length; for (let i = 0; i < numPresent; i++) { const key = presentKeys[i]; if (targetKeys.indexOf(key) === -1 && !exitingChildren.has(key)) { exitingChildren.set(key, undefined); } } // If we currently have exiting children, and we're deferring rendering incoming children // until after all current children have exiting, empty the childrenToRender array if (mode === "wait" && exitingChildren.size) { childrenToRender = []; } // Loop through all currently exiting components and clone them to overwrite `animate` // with any `exit` prop they might have defined. exitingChildren.forEach((component, key) => { // If this component is actually entering again, early return if (targetKeys.indexOf(key) !== -1) return; const child = allChildren.get(key); if (!child) return; const insertionIndex = presentKeys.indexOf(key); let exitingComponent = component; if (!exitingComponent) { const onExit = () => { // clean up the exiting children map exitingChildren.delete(key); // compute the keys of children that were rendered once but are no longer present // this could happen in case of too many fast consequent renderings // @link https://github.com/framer/motion/issues/2023 const leftOverKeys = Array.from(allChildren.keys()).filter((childKey) => !targetKeys.includes(childKey)); // clean up the all children map leftOverKeys.forEach((leftOverKey) => allChildren.delete(leftOverKey)); // make sure to render only the children that are actually visible presentChildren.current = filteredChildren.filter((presentChild) => { const presentChildKey = getChildKey(presentChild); return ( // filter out the node exiting presentChildKey === key || // filter out the leftover children leftOverKeys.includes(presentChildKey)); }); // Defer re-rendering until all exiting children have indeed left if (!exitingChildren.size) { if (isMounted.current === false) return; forceRender(); onExitComplete && onExitComplete(); } }; exitingComponent = (React.createElement(PresenceChild, { key: getChildKey(child), isPresent: false, onExitComplete: onExit, custom: custom, presenceAffectsLayout: presenceAffectsLayout, mode: mode }, child)); exitingChildren.set(key, exitingComponent); } childrenToRender.splice(insertionIndex, 0, exitingComponent); }); // Add `MotionContext` even to children that don't need it to ensure we're rendering // the same tree between renders childrenToRender = childrenToRender.map((child) => { const key = child.key; return exitingChildren.has(key) ? (child) : (React.createElement(PresenceChild, { key: getChildKey(child), isPresent: true, presenceAffectsLayout: presenceAffectsLayout, mode: mode }, child)); }); if (process.env.NODE_ENV !== "production" && mode === "wait" && childrenToRender.length > 1) { console.warn(`You're attempting to animate multiple children within AnimatePresence, but its mode is set to "wait". This will lead to odd visual behaviour.`); } return (React.createElement(React.Fragment, null, exitingChildren.size ? childrenToRender : childrenToRender.map((child) => cloneElement(child)))); }; export { AnimatePresence };