From 2870768a6d07a9872fde583835d2664a8639dadc Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Wed, 24 Jul 2024 15:42:02 +0200 Subject: [PATCH] Revert "Ensure AnimatePresence executes exiting animations in sequence (#2477)" (#2740) This reverts commit f83739f9c64011af8449b808251dcf60cd5be7b1. --- .../__tests__/AnimatePresence.test.tsx | 66 ++----------------- .../src/components/AnimatePresence/index.tsx | 49 ++++++++------ .../src/utils/use-force-update.ts | 4 +- 3 files changed, 37 insertions(+), 82 deletions(-) diff --git a/packages/framer-motion/src/components/AnimatePresence/__tests__/AnimatePresence.test.tsx b/packages/framer-motion/src/components/AnimatePresence/__tests__/AnimatePresence.test.tsx index b05437c379..a937f79bf8 100644 --- a/packages/framer-motion/src/components/AnimatePresence/__tests__/AnimatePresence.test.tsx +++ b/packages/framer-motion/src/components/AnimatePresence/__tests__/AnimatePresence.test.tsx @@ -338,7 +338,7 @@ describe("AnimatePresence", () => { key={i} animate={{ opacity: 1 }} exit={{ opacity: 0 }} - transition={{ duration: 0.1 }} + transition={{ duration: 0.5 }} /> ) @@ -354,7 +354,7 @@ describe("AnimatePresence", () => { rerender() rerender() resolve(container.childElementCount) - }, 200) + }, 150) }) return await expect(promise).resolves.toBe(1) @@ -419,8 +419,8 @@ describe("AnimatePresence", () => { // wait for the exit animation to check the DOM again setTimeout(() => { resolve(getByTestId("2").textContent === "2") - }, 150) - }, 200) + }, 250) + }, 150) }) return await expect(promise).resolves.toBeTruthy() @@ -454,67 +454,13 @@ describe("AnimatePresence", () => { // wait for the exit animation to check the DOM again setTimeout(() => { resolve(getByTestId("2").textContent === "2") - }, 150) - }, 200) + }, 250) + }, 150) }) return await expect(promise).resolves.toBeTruthy() }) - test("Elements exit in sequence during fast renders", async () => { - const Component = ({ nums }: { nums: number[] }) => { - return ( - - {nums.map((i) => ( - - {i} - - ))} - - ) - } - - const { rerender, getAllByTestId } = render( - - ) - - const getTextContents = () => { - return getAllByTestId(/./).flatMap((element) => - element.textContent !== null - ? parseInt(element.textContent) - : [] - ) - } - - await new Promise((resolve) => { - setTimeout(() => { - act(() => rerender()) - setTimeout(() => { - expect(getTextContents()).toEqual([1, 2, 3]) - }, 100) - }, 100) - setTimeout(() => { - act(() => rerender()) - setTimeout(() => { - expect(getTextContents()).toEqual([2, 3]) - }, 100) - }, 250) - setTimeout(() => { - act(() => rerender()) - setTimeout(() => { - expect(getTextContents()).toEqual([3]) - resolve() - }, 100) - }, 400) - }) - }) - test("Exit variants are triggered with `AnimatePresence.custom`, not that of the element.", async () => { const variants = { enter: { x: 0, transition: { type: false } }, diff --git a/packages/framer-motion/src/components/AnimatePresence/index.tsx b/packages/framer-motion/src/components/AnimatePresence/index.tsx index 7507ceb9af..108c36f14c 100644 --- a/packages/framer-motion/src/components/AnimatePresence/index.tsx +++ b/packages/framer-motion/src/components/AnimatePresence/index.tsx @@ -96,14 +96,17 @@ export const AnimatePresence: React.FunctionComponent< const isMounted = useIsMounted() // Filter out any children that aren't ReactElements. We can only track ReactElements with a props.key - const filteredChildren = useRef(onlyElements(children)) - filteredChildren.current = onlyElements(children) - let childrenToRender = filteredChildren.current + const filteredChildren = onlyElements(children) + let childrenToRender = filteredChildren const exitingChildren = useRef( new Map | undefined>() ).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>() @@ -115,7 +118,9 @@ export const AnimatePresence: React.FunctionComponent< useIsomorphicLayoutEffect(() => { isInitialRender.current = false - updateChildLookup(filteredChildren.current, allChildren) + + updateChildLookup(filteredChildren, allChildren) + presentChildren.current = childrenToRender }) useUnmountEffect(() => { @@ -147,8 +152,8 @@ export const AnimatePresence: React.FunctionComponent< // Diff the keys of the currently-present and target children to update our // exiting list. - const presentKeys = Array.from(allChildren.keys()) - const targetKeys = filteredChildren.current.map(getChildKey) + 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 @@ -168,12 +173,12 @@ export const AnimatePresence: React.FunctionComponent< // Loop through all currently exiting components and clone them to overwrite `animate` // with any `exit` prop they might have defined. - for (const [key, component] of exitingChildren) { + exitingChildren.forEach((component, key) => { // If this component is actually entering again, early return - if (targetKeys.indexOf(key) !== -1) continue + if (targetKeys.indexOf(key) !== -1) return const child = allChildren.get(key) - if (!child) continue + if (!child) return const insertionIndex = presentKeys.indexOf(key) @@ -183,16 +188,6 @@ export const AnimatePresence: React.FunctionComponent< // clean up the exiting children map exitingChildren.delete(key) - // Accounts for the edge case where there are still exiting children when the - // children list is already empty from React's POV, which results in React not - // auto re-rendering - if ( - filteredChildren.current.length === 0 && - exitingChildren.size > 0 - ) { - forceRender() - } - // 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 @@ -205,6 +200,20 @@ export const AnimatePresence: React.FunctionComponent< 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 @@ -230,7 +239,7 @@ export const AnimatePresence: React.FunctionComponent< } 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 diff --git a/packages/framer-motion/src/utils/use-force-update.ts b/packages/framer-motion/src/utils/use-force-update.ts index 2d08fc477b..28b7f0d5d8 100644 --- a/packages/framer-motion/src/utils/use-force-update.ts +++ b/packages/framer-motion/src/utils/use-force-update.ts @@ -7,8 +7,8 @@ export function useForceUpdate(): [VoidFunction, number] { const [forcedRenderCount, setForcedRenderCount] = useState(0) const forceRender = useCallback(() => { - isMounted.current && setForcedRenderCount((count) => count + 1) - }, [isMounted]) + isMounted.current && setForcedRenderCount(forcedRenderCount + 1) + }, [forcedRenderCount]) /** * Defer this to the end of the next animation frame in case there are multiple