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