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 eb39373019..0bf3b62958 100644
--- a/packages/framer-motion/src/components/AnimatePresence/__tests__/AnimatePresence.test.tsx
+++ b/packages/framer-motion/src/components/AnimatePresence/__tests__/AnimatePresence.test.tsx
@@ -329,7 +329,7 @@ describe("AnimatePresence", () => {
key={i}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
- transition={{ duration: 0.5 }}
+ transition={{ duration: 0.1 }}
/>
)
@@ -345,7 +345,7 @@ describe("AnimatePresence", () => {
rerender()
rerender()
resolve(container.childElementCount)
- }, 150)
+ }, 200)
})
return await expect(promise).resolves.toBe(1)
@@ -410,8 +410,8 @@ describe("AnimatePresence", () => {
// wait for the exit animation to check the DOM again
setTimeout(() => {
resolve(getByTestId("2").textContent === "2")
- }, 250)
- }, 150)
+ }, 150)
+ }, 200)
})
return await expect(promise).resolves.toBeTruthy()
@@ -445,13 +445,67 @@ describe("AnimatePresence", () => {
// wait for the exit animation to check the DOM again
setTimeout(() => {
resolve(getByTestId("2").textContent === "2")
- }, 250)
- }, 150)
+ }, 150)
+ }, 200)
})
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 108c36f14c..7507ceb9af 100644
--- a/packages/framer-motion/src/components/AnimatePresence/index.tsx
+++ b/packages/framer-motion/src/components/AnimatePresence/index.tsx
@@ -96,17 +96,14 @@ 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 = onlyElements(children)
- let childrenToRender = filteredChildren
+ const filteredChildren = useRef(onlyElements(children))
+ filteredChildren.current = onlyElements(children)
+ let childrenToRender = filteredChildren.current
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>()
@@ -118,9 +115,7 @@ export const AnimatePresence: React.FunctionComponent<
useIsomorphicLayoutEffect(() => {
isInitialRender.current = false
-
- updateChildLookup(filteredChildren, allChildren)
- presentChildren.current = childrenToRender
+ updateChildLookup(filteredChildren.current, allChildren)
})
useUnmountEffect(() => {
@@ -152,8 +147,8 @@ export const AnimatePresence: React.FunctionComponent<
// 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)
+ const presentKeys = Array.from(allChildren.keys())
+ const targetKeys = filteredChildren.current.map(getChildKey)
// Diff the present children with our target children and mark those that are exiting
const numPresent = presentKeys.length
@@ -173,12 +168,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.
- exitingChildren.forEach((component, key) => {
+ for (const [key, component] of exitingChildren) {
// If this component is actually entering again, early return
- if (targetKeys.indexOf(key) !== -1) return
+ if (targetKeys.indexOf(key) !== -1) continue
const child = allChildren.get(key)
- if (!child) return
+ if (!child) continue
const insertionIndex = presentKeys.indexOf(key)
@@ -188,6 +183,16 @@ 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
@@ -200,20 +205,6 @@ 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
@@ -239,7 +230,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 28b7f0d5d8..2d08fc477b 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(forcedRenderCount + 1)
- }, [forcedRenderCount])
+ isMounted.current && setForcedRenderCount((count) => count + 1)
+ }, [isMounted])
/**
* Defer this to the end of the next animation frame in case there are multiple