From 29c4a5bdc1367910271b1efe2aec68c1a9491ba4 Mon Sep 17 00:00:00 2001 From: JaeSeoKim Date: Fri, 3 Jun 2022 19:07:14 +0900 Subject: [PATCH 1/9] Fix AnimatePresence won't unmount fastly changing content fixed: #907 #1439 #1534 --- packages/framer-motion/src/components/AnimatePresence/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/framer-motion/src/components/AnimatePresence/index.tsx b/packages/framer-motion/src/components/AnimatePresence/index.tsx index d652df695c..1c2e420f1d 100644 --- a/packages/framer-motion/src/components/AnimatePresence/index.tsx +++ b/packages/framer-motion/src/components/AnimatePresence/index.tsx @@ -105,7 +105,7 @@ export const AnimatePresence: React.FunctionComponent< const filteredChildren = onlyElements(children) let childrenToRender = filteredChildren - const exiting = new Set() + const exiting = useRef(new Set()).current // Keep a living record of the children we're actually rendering so we // can diff to figure out which are entering and exiting From e728b7f12a94ca4ef91ba8190517016459a08935 Mon Sep 17 00:00:00 2001 From: JaeSeoKim Date: Sat, 11 Jun 2022 00:44:26 +0900 Subject: [PATCH 2/9] Change the AnimatePresence childrenToRender algorithm --- .../src/components/AnimatePresence/index.tsx | 207 ++++++++++++------ 1 file changed, 141 insertions(+), 66 deletions(-) diff --git a/packages/framer-motion/src/components/AnimatePresence/index.tsx b/packages/framer-motion/src/components/AnimatePresence/index.tsx index 1c2e420f1d..9dbeead29b 100644 --- a/packages/framer-motion/src/components/AnimatePresence/index.tsx +++ b/packages/framer-motion/src/components/AnimatePresence/index.tsx @@ -43,6 +43,32 @@ function onlyElements(children: ReactNode): ReactElement[] { return filtered } +function splitChildrenByKeys( + keys: ComponentKey[], + children: ReactElement[], + mapFunction?: (child: ReactElement) => ReactElement +): ReactElement[][] { + const chunks: ReactElement[][] = [] + let insertionStartIndex = 0 + + keys.forEach((key) => { + const insertionEndIndex = children.findIndex( + (child) => getChildKey(child) === key + ) + + let chunk = children.slice(insertionStartIndex, insertionEndIndex) + if (mapFunction) chunk = chunk.map(mapFunction) + chunks.push(chunk) + insertionStartIndex = insertionEndIndex + 1 + }) + + let chunk = children.slice(insertionStartIndex, children.length) + if (mapFunction) chunk = chunk.map(mapFunction) + chunks.push(chunk) + + return chunks +} + /** * `AnimatePresence` enables the animation of components that have been removed from the tree. * @@ -105,7 +131,10 @@ export const AnimatePresence: React.FunctionComponent< const filteredChildren = onlyElements(children) let childrenToRender = filteredChildren - const exiting = useRef(new Set()).current + // Keep a living record of the children we're actually rendering so we + 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 @@ -130,7 +159,7 @@ export const AnimatePresence: React.FunctionComponent< useUnmountEffect(() => { isInitialRender.current = true allChildren.clear() - exiting.clear() + exitingChildren.clear() }) if (isInitialRender.current) { @@ -152,84 +181,78 @@ export const AnimatePresence: React.FunctionComponent< } // 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. + // preserving list. const presentKeys = presentChildren.current.map(getChildKey) const targetKeys = filteredChildren.map(getChildKey) + const preservingKeys: ComponentKey[] = [] - // Diff the present children with our target children and mark those that are exiting + // Diff the present children with our target children and mark those that are preserving const numPresent = presentKeys.length for (let i = 0; i < numPresent; i++) { const key = presentKeys[i] - if (targetKeys.indexOf(key) === -1) { - exiting.add(key) + if (targetKeys.indexOf(key) !== -1) { + preservingKeys.push(key) } } - // 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" && exiting.size) { - childrenToRender = [] - } - - // Loop through all currently exiting components and clone them to overwrite `animate` - // with any `exit` prop they might have defined. - exiting.forEach((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) - - const onExit = () => { - allChildren.delete(key) - exiting.delete(key) - - // Remove this child from the present children - const removeIndex = presentChildren.current.findIndex( - (presentChild) => presentChild.key === key - ) - presentChildren.current.splice(removeIndex, 1) - - // Defer re-rendering until all exiting children have indeed left - if (!exiting.size) { - presentChildren.current = filteredChildren - - if (isMounted.current === false) return - - forceRender() - onExitComplete && onExitComplete() + // split the presentChildren based on the key of the component you are preserving + const presentChunks = splitChildrenByKeys( + preservingKeys, + presentChildren.current, + (_child) => { + const key = getChildKey(_child) + const child = allChildren.get(key)! + + // If the component was exiting, reuse the previous component to preserve state + let extingChild = exitingChildren.get(key) + if (extingChild) return extingChild + + const onExit = () => { + allChildren.delete(key) + exitingChildren.delete(key) + + // Remove this child from the present children + const removeIndex = presentChildren.current.findIndex( + (presentChild) => presentChild.key === key + ) + presentChildren.current.splice(removeIndex, 1) + + // Defer re-rendering until all exiting children have indeed left + if (!exitingChildren.size) { + presentChildren.current = filteredChildren + + if (isMounted.current === false) return + + forceRender() + onExitComplete && onExitComplete() + } } + extingChild = ( + + {child} + + ) + exitingChildren.set(key, extingChild) + return extingChild } + ) - childrenToRender.splice( - insertionIndex, - 0, - - {child} - - ) - }) - - // 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 as string | number - return exiting.has(key) ? ( - child - ) : ( + const targetChunks = splitChildrenByKeys( + preservingKeys, + filteredChildren, + (child) => ( + // Add `MotionContext` even to children that don't need it to ensure we're rendering + // the same tree between renders ) + ) + + // Combine the chunk separated by the preservingKeys. + // + // If a change occurs in the rendering array, + // insert the chunk where the change occurred in the previous location. + // + // presentChildren -> children + // [1] [A] + // [A] [D] + // [2] [E] + // [B] [F] + // [3] [B] + // [C] [C] + // + // init -> animate -> Exit Complete + // + // [1] [1] <--- presentChunk - 1 + // [A] [A] [A] <--- preservingKey + // [2] [2] <--- presentChunk - 2 + // [D] [D] + // [E] [E] <--- targetChunk - 1 + // [F] [F] + // [B] [B] [B] <--- preservingKey + // [3] [3] <--- presentChunk - 3 + // [B] <--- targetChunk - 2 + // [C] [C] [C] <--- preservingKey + childrenToRender = [] + Array.from({ length: preservingKeys.length + 1 }).forEach((_, i) => { + const key = preservingKeys[i] + const child = allChildren.get(key) + + childrenToRender = childrenToRender.concat(presentChunks[i]) + + // 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 = childrenToRender.concat(targetChunks[i]) + } + + if (child) { + childrenToRender.push( + + {child} + + ) + } }) if ( @@ -253,7 +328,7 @@ export const AnimatePresence: React.FunctionComponent< return ( <> - {exiting.size + {exitingChildren.size ? childrenToRender : childrenToRender.map((child) => cloneElement(child))} From 037e7c7d5570ca15adc2d5686cdb5907ec7bb617 Mon Sep 17 00:00:00 2001 From: JaeSeoKim Date: Sat, 11 Jun 2022 00:48:21 +0900 Subject: [PATCH 3/9] Remove some comments --- packages/framer-motion/src/components/AnimatePresence/index.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/framer-motion/src/components/AnimatePresence/index.tsx b/packages/framer-motion/src/components/AnimatePresence/index.tsx index 9dbeead29b..a43e520079 100644 --- a/packages/framer-motion/src/components/AnimatePresence/index.tsx +++ b/packages/framer-motion/src/components/AnimatePresence/index.tsx @@ -131,7 +131,6 @@ export const AnimatePresence: React.FunctionComponent< const filteredChildren = onlyElements(children) let childrenToRender = filteredChildren - // Keep a living record of the children we're actually rendering so we const exitingChildren = useRef( new Map>() ).current From 75c84c4ce91845089e862a418c96c806a8a8a50d Mon Sep 17 00:00:00 2001 From: JaeSeoKim Date: Sat, 11 Jun 2022 01:06:26 +0900 Subject: [PATCH 4/9] Update comment for AnimatePresence --- .../src/components/AnimatePresence/index.tsx | 33 ++++++++++--------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/packages/framer-motion/src/components/AnimatePresence/index.tsx b/packages/framer-motion/src/components/AnimatePresence/index.tsx index a43e520079..0de93ea2aa 100644 --- a/packages/framer-motion/src/components/AnimatePresence/index.tsx +++ b/packages/framer-motion/src/components/AnimatePresence/index.tsx @@ -269,25 +269,26 @@ export const AnimatePresence: React.FunctionComponent< // insert the chunk where the change occurred in the previous location. // // presentChildren -> children - // [1] [A] - // [A] [D] - // [2] [E] - // [B] [F] - // [3] [B] + // [A] [1] + // [D] [A] + // [E] [2] + // [F] [B] + // [B] [3] // [C] [C] // - // init -> animate -> Exit Complete + // init -> animate -> Exit Complete // - // [1] [1] <--- presentChunk - 1 - // [A] [A] [A] <--- preservingKey - // [2] [2] <--- presentChunk - 2 - // [D] [D] - // [E] [E] <--- targetChunk - 1 - // [F] [F] - // [B] [B] [B] <--- preservingKey - // [3] [3] <--- presentChunk - 3 - // [B] <--- targetChunk - 2 - // [C] [C] [C] <--- preservingKey + // [1] [1] <--- presentChunk - 1 + // [A] [A] [A] <--- preservingKey + // [2] [2] <--- presentChunk - 2 + // [D] [D] + // [E] [E] <--- targetChunk - 1 + // [F] [F] + // [B] [B] [B] <--- preservingKey + // [3] [3] <--- presentChunk - 3 + // [B] <--- targetChunk - 2 + // [C] [C] [C] <--- preservingKey + childrenToRender = [] Array.from({ length: preservingKeys.length + 1 }).forEach((_, i) => { const key = preservingKeys[i] From 0e4eafe526e6b15f3539eed567fe8fbf886ab973 Mon Sep 17 00:00:00 2001 From: JaeSeoKim Date: Sat, 11 Jun 2022 01:07:39 +0900 Subject: [PATCH 5/9] Fix some comments --- packages/framer-motion/src/components/AnimatePresence/index.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/framer-motion/src/components/AnimatePresence/index.tsx b/packages/framer-motion/src/components/AnimatePresence/index.tsx index 0de93ea2aa..2ba605b082 100644 --- a/packages/framer-motion/src/components/AnimatePresence/index.tsx +++ b/packages/framer-motion/src/components/AnimatePresence/index.tsx @@ -286,7 +286,6 @@ export const AnimatePresence: React.FunctionComponent< // [F] [F] // [B] [B] [B] <--- preservingKey // [3] [3] <--- presentChunk - 3 - // [B] <--- targetChunk - 2 // [C] [C] [C] <--- preservingKey childrenToRender = [] From cabebf56549ea46144b0904f1f4e3003eb4e1d34 Mon Sep 17 00:00:00 2001 From: JaeSeoKim Date: Sat, 11 Jun 2022 01:34:48 +0900 Subject: [PATCH 6/9] Fix AnimatePresence checking all exiting children --- .../src/components/AnimatePresence/index.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/framer-motion/src/components/AnimatePresence/index.tsx b/packages/framer-motion/src/components/AnimatePresence/index.tsx index 2ba605b082..e0f563ec8b 100644 --- a/packages/framer-motion/src/components/AnimatePresence/index.tsx +++ b/packages/framer-motion/src/components/AnimatePresence/index.tsx @@ -131,6 +131,7 @@ export const AnimatePresence: React.FunctionComponent< const filteredChildren = onlyElements(children) let childrenToRender = filteredChildren + const exiting = useRef(new Set()).current const exitingChildren = useRef( new Map>() ).current @@ -158,6 +159,7 @@ export const AnimatePresence: React.FunctionComponent< useUnmountEffect(() => { isInitialRender.current = true allChildren.clear() + exiting.clear() exitingChildren.clear() }) @@ -192,7 +194,11 @@ export const AnimatePresence: React.FunctionComponent< for (let i = 0; i < numPresent; i++) { const key = presentKeys[i] - if (targetKeys.indexOf(key) !== -1) { + if (targetKeys.indexOf(key) === -1) { + exiting.add(key) + } else { + exiting.delete(key) + exitingChildren.delete(key) preservingKeys.push(key) } } @@ -211,6 +217,7 @@ export const AnimatePresence: React.FunctionComponent< const onExit = () => { allChildren.delete(key) + exiting.delete(key) exitingChildren.delete(key) // Remove this child from the present children @@ -220,7 +227,7 @@ export const AnimatePresence: React.FunctionComponent< presentChildren.current.splice(removeIndex, 1) // Defer re-rendering until all exiting children have indeed left - if (!exitingChildren.size) { + if (!exiting.size) { presentChildren.current = filteredChildren if (isMounted.current === false) return From b784cb736e6148aca64a223f2ae6d974f037bf19 Mon Sep 17 00:00:00 2001 From: JaeSeoKim Date: Sat, 11 Jun 2022 02:02:18 +0900 Subject: [PATCH 7/9] Fix comment for AnimatePresence --- .../src/components/AnimatePresence/index.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/framer-motion/src/components/AnimatePresence/index.tsx b/packages/framer-motion/src/components/AnimatePresence/index.tsx index e0f563ec8b..9da6a06bcb 100644 --- a/packages/framer-motion/src/components/AnimatePresence/index.tsx +++ b/packages/framer-motion/src/components/AnimatePresence/index.tsx @@ -285,14 +285,14 @@ export const AnimatePresence: React.FunctionComponent< // // init -> animate -> Exit Complete // - // [1] [1] <--- presentChunk - 1 + // [1] [1] <--- targetChunk - 1 // [A] [A] [A] <--- preservingKey - // [2] [2] <--- presentChunk - 2 // [D] [D] - // [E] [E] <--- targetChunk - 1 + // [E] [E] <--- presentChunk - 1 // [F] [F] + // [2] [2] <--- targetChunk - 2 // [B] [B] [B] <--- preservingKey - // [3] [3] <--- presentChunk - 3 + // [3] [3] <--- targetChunk - 3 // [C] [C] [C] <--- preservingKey childrenToRender = [] From b6f70644a03629f9a756ac32072470582f2cec82 Mon Sep 17 00:00:00 2001 From: JaeSeoKim Date: Mon, 13 Jun 2022 14:39:08 +0900 Subject: [PATCH 8/9] Remove unnecessary AnimatePresence variable --- .../src/components/AnimatePresence/index.tsx | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/packages/framer-motion/src/components/AnimatePresence/index.tsx b/packages/framer-motion/src/components/AnimatePresence/index.tsx index 9da6a06bcb..14d7e475df 100644 --- a/packages/framer-motion/src/components/AnimatePresence/index.tsx +++ b/packages/framer-motion/src/components/AnimatePresence/index.tsx @@ -131,9 +131,8 @@ export const AnimatePresence: React.FunctionComponent< const filteredChildren = onlyElements(children) let childrenToRender = filteredChildren - const exiting = useRef(new Set()).current const exitingChildren = useRef( - new Map>() + new Map | undefined>() ).current // Keep a living record of the children we're actually rendering so we @@ -159,7 +158,6 @@ export const AnimatePresence: React.FunctionComponent< useUnmountEffect(() => { isInitialRender.current = true allChildren.clear() - exiting.clear() exitingChildren.clear() }) @@ -195,11 +193,10 @@ export const AnimatePresence: React.FunctionComponent< const key = presentKeys[i] if (targetKeys.indexOf(key) === -1) { - exiting.add(key) + exitingChildren.set(key, undefined) } else { - exiting.delete(key) - exitingChildren.delete(key) preservingKeys.push(key) + exitingChildren.delete(key) } } @@ -217,7 +214,6 @@ export const AnimatePresence: React.FunctionComponent< const onExit = () => { allChildren.delete(key) - exiting.delete(key) exitingChildren.delete(key) // Remove this child from the present children @@ -227,7 +223,7 @@ export const AnimatePresence: React.FunctionComponent< presentChildren.current.splice(removeIndex, 1) // Defer re-rendering until all exiting children have indeed left - if (!exiting.size) { + if (!exitingChildren.size) { presentChildren.current = filteredChildren if (isMounted.current === false) return From 43489669e2d8584e7191297a9cb0ee4647d28d53 Mon Sep 17 00:00:00 2001 From: JaeSeoKim Date: Wed, 17 Aug 2022 14:46:31 +0000 Subject: [PATCH 9/9] Change AnimatePresence memoization logic --- .../AnimatePresence/PresenceChild.tsx | 9 +- .../src/components/AnimatePresence/index.tsx | 203 +++++++++--------- 2 files changed, 112 insertions(+), 100 deletions(-) diff --git a/packages/framer-motion/src/components/AnimatePresence/PresenceChild.tsx b/packages/framer-motion/src/components/AnimatePresence/PresenceChild.tsx index 3dacaf5894..9843edfc6c 100644 --- a/packages/framer-motion/src/components/AnimatePresence/PresenceChild.tsx +++ b/packages/framer-motion/src/components/AnimatePresence/PresenceChild.tsx @@ -1,5 +1,6 @@ import * as React from "react" import { useId, useMemo } from "react" +import { ComponentKey } from "." import { PresenceContext, PresenceContextProps, @@ -11,11 +12,12 @@ import { PopChild } from "./PopChild" interface PresenceChildProps { children: React.ReactElement isPresent: boolean - onExitComplete?: () => void + onExitComplete?: (key: ComponentKey) => void initial?: false | VariantLabels custom?: any presenceAffectsLayout: boolean mode: "sync" | "popLayout" | "wait" + childKey: ComponentKey } export const PresenceChild = ({ @@ -26,6 +28,7 @@ export const PresenceChild = ({ custom, presenceAffectsLayout, mode, + childKey, }: PresenceChildProps) => { const presenceChildren = useConstant(newChildrenMap) const id = useId() @@ -43,7 +46,7 @@ export const PresenceChild = ({ if (!isComplete) return // can stop searching when any is incomplete } - onExitComplete?.() + onExitComplete?.(childKey) }, register: (childId: string) => { presenceChildren.set(childId, false) @@ -67,7 +70,7 @@ export const PresenceChild = ({ * component immediately. */ React.useEffect(() => { - !isPresent && !presenceChildren.size && onExitComplete?.() + !isPresent && !presenceChildren.size && onExitComplete?.(childKey) }, [isPresent]) if (mode === "popLayout") { diff --git a/packages/framer-motion/src/components/AnimatePresence/index.tsx b/packages/framer-motion/src/components/AnimatePresence/index.tsx index 14d7e475df..00d22a0beb 100644 --- a/packages/framer-motion/src/components/AnimatePresence/index.tsx +++ b/packages/framer-motion/src/components/AnimatePresence/index.tsx @@ -1,38 +1,39 @@ import { useRef, + useCallback, isValidElement, cloneElement, Children, ReactElement, ReactNode, useContext, -} from "react" -import * as React from "react" -import { env } from "../../utils/process" -import { AnimatePresenceProps } from "./types" -import { useForceUpdate } from "../../utils/use-force-update" -import { useIsMounted } from "../../utils/use-is-mounted" -import { PresenceChild } from "./PresenceChild" -import { LayoutGroupContext } from "../../context/LayoutGroupContext" -import { useIsomorphicLayoutEffect } from "../../utils/use-isomorphic-effect" -import { useUnmountEffect } from "../../utils/use-unmount-effect" -import { warnOnce } from "../../utils/warn-once" - -type ComponentKey = string | number - -const getChildKey = (child: ReactElement): ComponentKey => child.key || "" - -function updateChildLookup( + } from "react" + import * as React from "react" + import { env } from "../../utils/process" + import { AnimatePresenceProps } from "./types" + import { useForceUpdate } from "../../utils/use-force-update" + import { useIsMounted } from "../../utils/use-is-mounted" + import { PresenceChild } from "./PresenceChild" + import { LayoutGroupContext } from "../../context/LayoutGroupContext" + import { useIsomorphicLayoutEffect } from "../../utils/use-isomorphic-effect" + import { useUnmountEffect } from "../../utils/use-unmount-effect" + import { warnOnce } from "../../utils/warn-once" + + export type ComponentKey = string | number + + const getChildKey = (child: ReactElement): ComponentKey => child.key || "" + + function updateChildLookup( children: ReactElement[], allChildren: Map> -) { + ) { children.forEach((child) => { const key = getChildKey(child) allChildren.set(key, child) }) -} + } -function onlyElements(children: ReactNode): ReactElement[] { + function onlyElements(children: ReactNode): ReactElement[] { const filtered: ReactElement[] = [] // We use forEach here instead of map as map mutates the component key by preprending `.$` @@ -41,13 +42,13 @@ function onlyElements(children: ReactNode): ReactElement[] { }) return filtered -} + } -function splitChildrenByKeys( + function splitChildrenByKeys( keys: ComponentKey[], children: ReactElement[], mapFunction?: (child: ReactElement) => ReactElement -): ReactElement[][] { + ): ReactElement[][] { const chunks: ReactElement[][] = [] let insertionStartIndex = 0 @@ -67,44 +68,44 @@ function splitChildrenByKeys( chunks.push(chunk) return chunks -} - -/** - * `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 - */ -export const AnimatePresence: React.FunctionComponent< + } + + /** + * `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 + */ + export const AnimatePresence: React.FunctionComponent< React.PropsWithChildren -> = ({ + > = ({ children, custom, initial = true, @@ -112,7 +113,7 @@ export const AnimatePresence: React.FunctionComponent< exitBeforeEnter, presenceAffectsLayout = true, mode = "sync", -}) => { + }) => { // Support deprecated exitBeforeEnter prop if (exitBeforeEnter) { mode = "wait" @@ -131,9 +132,7 @@ export const AnimatePresence: React.FunctionComponent< const filteredChildren = onlyElements(children) let childrenToRender = filteredChildren - const exitingChildren = useRef( - new Map | undefined>() - ).current + const exiting = useRef(new Set()).current // Keep a living record of the children we're actually rendering so we // can diff to figure out which are entering and exiting @@ -148,6 +147,37 @@ export const AnimatePresence: React.FunctionComponent< // we play onMount animations or not. const isInitialRender = useRef(true) + const onPresenceChildRemove = useCallback( + (key: ComponentKey) => { + allChildren.delete(key) + exiting.delete(key) + + // Remove this child from the present children + const removeIndex = presentChildren.current.findIndex( + (presentChild) => presentChild.key === key + ) + presentChildren.current.splice(removeIndex, 1) + + // Defer re-rendering until all exiting children have indeed left + if (!exiting.size) { + presentChildren.current = filteredChildren + + if (isMounted.current === false) return + + forceRender() + onExitComplete && onExitComplete() + } + }, + [ + allChildren, + exiting, + filteredChildren, + forceRender, + isMounted, + onExitComplete, + ] + ) + useIsomorphicLayoutEffect(() => { isInitialRender.current = false @@ -158,7 +188,7 @@ export const AnimatePresence: React.FunctionComponent< useUnmountEffect(() => { isInitialRender.current = true allChildren.clear() - exitingChildren.clear() + exiting.clear() }) if (isInitialRender.current) { @@ -167,6 +197,7 @@ export const AnimatePresence: React.FunctionComponent< {childrenToRender.map((child) => ( { - allChildren.delete(key) - exitingChildren.delete(key) - - // Remove this child from the present children - const removeIndex = presentChildren.current.findIndex( - (presentChild) => presentChild.key === key - ) - presentChildren.current.splice(removeIndex, 1) - - // Defer re-rendering until all exiting children have indeed left - if (!exitingChildren.size) { - presentChildren.current = filteredChildren - - if (isMounted.current === false) return - - forceRender() - onExitComplete && onExitComplete() - } - } - extingChild = ( + const extingChild = ( ) - exitingChildren.set(key, extingChild) return extingChild } ) @@ -257,6 +264,7 @@ export const AnimatePresence: React.FunctionComponent< // the same tree between renders - {exitingChildren.size + {exiting.size ? childrenToRender : childrenToRender.map((child) => cloneElement(child))} ) -} + }