Skip to content

Commit

Permalink
Fix sequence bug in AnimatePresence (framer#2462)
Browse files Browse the repository at this point in the history
  • Loading branch information
regexyl committed Jan 4, 2024
1 parent 4785efa commit 4727856
Show file tree
Hide file tree
Showing 2 changed files with 21 additions and 28 deletions.
45 changes: 19 additions & 26 deletions packages/framer-motion/src/components/AnimatePresence/index.tsx
Expand Up @@ -96,16 +96,15 @@ 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))
let childrenToRender = filteredChildren.current

const exitingChildren = useRef(
new Map<ComponentKey, ReactElement<any> | 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(
Expand All @@ -118,9 +117,7 @@ export const AnimatePresence: React.FunctionComponent<

useIsomorphicLayoutEffect(() => {
isInitialRender.current = false

updateChildLookup(filteredChildren, allChildren)
presentChildren.current = childrenToRender
updateChildLookup(filteredChildren.current, allChildren)
})

useUnmountEffect(() => {
Expand Down Expand Up @@ -152,8 +149,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
Expand All @@ -173,12 +170,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)

Expand All @@ -188,6 +185,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
Expand All @@ -200,20 +207,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
Expand All @@ -239,7 +232,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
Expand Down
4 changes: 2 additions & 2 deletions packages/framer-motion/src/utils/use-force-update.ts
Expand Up @@ -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
Expand Down

0 comments on commit 4727856

Please sign in to comment.