Skip to content

Commit

Permalink
Change AnimatePresence memoization logic
Browse files Browse the repository at this point in the history
  • Loading branch information
JaeSeoKim committed Aug 17, 2022
1 parent b6f7064 commit 4348966
Show file tree
Hide file tree
Showing 2 changed files with 112 additions and 100 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as React from "react"
import { useId, useMemo } from "react"
import { ComponentKey } from "."
import {
PresenceContext,
PresenceContextProps,
Expand All @@ -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 = ({
Expand All @@ -26,6 +28,7 @@ export const PresenceChild = ({
custom,
presenceAffectsLayout,
mode,
childKey,
}: PresenceChildProps) => {
const presenceChildren = useConstant(newChildrenMap)
const id = useId()
Expand All @@ -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)
Expand All @@ -67,7 +70,7 @@ export const PresenceChild = ({
* component immediately.
*/
React.useEffect(() => {
!isPresent && !presenceChildren.size && onExitComplete?.()
!isPresent && !presenceChildren.size && onExitComplete?.(childKey)
}, [isPresent])

if (mode === "popLayout") {
Expand Down
203 changes: 106 additions & 97 deletions packages/framer-motion/src/components/AnimatePresence/index.tsx
Original file line number Diff line number Diff line change
@@ -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<any>): 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<any>): ComponentKey => child.key || ""

function updateChildLookup(
children: ReactElement<any>[],
allChildren: Map<ComponentKey, ReactElement<any>>
) {
) {
children.forEach((child) => {
const key = getChildKey(child)
allChildren.set(key, child)
})
}
}

function onlyElements(children: ReactNode): ReactElement<any>[] {
function onlyElements(children: ReactNode): ReactElement<any>[] {
const filtered: ReactElement<any>[] = []

// We use forEach here instead of map as map mutates the component key by preprending `.$`
Expand All @@ -41,13 +42,13 @@ function onlyElements(children: ReactNode): ReactElement<any>[] {
})

return filtered
}
}

function splitChildrenByKeys(
function splitChildrenByKeys(
keys: ComponentKey[],
children: ReactElement<any>[],
mapFunction?: (child: ReactElement<any>) => ReactElement<any>
): ReactElement<any>[][] {
): ReactElement<any>[][] {
const chunks: ReactElement<any>[][] = []
let insertionStartIndex = 0

Expand All @@ -67,52 +68,52 @@ 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 }) => (
* <AnimatePresence>
* {items.map(item => (
* <motion.div
* key={item.id}
* initial={{ opacity: 0 }}
* animate={{ opacity: 1 }}
* exit={{ opacity: 0 }}
* />
* ))}
* </AnimatePresence>
* )
* ```
*
* 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 }) => (
* <AnimatePresence>
* {items.map(item => (
* <motion.div
* key={item.id}
* initial={{ opacity: 0 }}
* animate={{ opacity: 1 }}
* exit={{ opacity: 0 }}
* />
* ))}
* </AnimatePresence>
* )
* ```
*
* 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<AnimatePresenceProps>
> = ({
> = ({
children,
custom,
initial = true,
onExitComplete,
exitBeforeEnter,
presenceAffectsLayout = true,
mode = "sync",
}) => {
}) => {
// Support deprecated exitBeforeEnter prop
if (exitBeforeEnter) {
mode = "wait"
Expand All @@ -131,9 +132,7 @@ export const AnimatePresence: React.FunctionComponent<
const filteredChildren = onlyElements(children)
let childrenToRender = filteredChildren

const exitingChildren = useRef(
new Map<ComponentKey, ReactElement<any> | undefined>()
).current
const exiting = useRef(new Set<ComponentKey>()).current

// Keep a living record of the children we're actually rendering so we
// can diff to figure out which are entering and exiting
Expand All @@ -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

Expand All @@ -158,7 +188,7 @@ export const AnimatePresence: React.FunctionComponent<
useUnmountEffect(() => {
isInitialRender.current = true
allChildren.clear()
exitingChildren.clear()
exiting.clear()
})

if (isInitialRender.current) {
Expand All @@ -167,6 +197,7 @@ export const AnimatePresence: React.FunctionComponent<
{childrenToRender.map((child) => (
<PresenceChild
key={getChildKey(child)}
childKey={getChildKey(child)}
isPresent
initial={initial ? undefined : false}
presenceAffectsLayout={presenceAffectsLayout}
Expand All @@ -193,10 +224,10 @@ export const AnimatePresence: React.FunctionComponent<
const key = presentKeys[i]

if (targetKeys.indexOf(key) === -1) {
exitingChildren.set(key, undefined)
exiting.add(key)
} else {
preservingKeys.push(key)
exitingChildren.delete(key)
exiting.delete(key)
}
}

Expand All @@ -208,43 +239,19 @@ export const AnimatePresence: React.FunctionComponent<
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 = (
const extingChild = (
<PresenceChild
key={key}
childKey={key}
isPresent={false}
onExitComplete={onExit}
onExitComplete={onPresenceChildRemove}
custom={custom}
presenceAffectsLayout={presenceAffectsLayout}
mode={mode}
>
{child}
</PresenceChild>
)
exitingChildren.set(key, extingChild)
return extingChild
}
)
Expand All @@ -257,6 +264,7 @@ export const AnimatePresence: React.FunctionComponent<
// the same tree between renders
<PresenceChild
key={getChildKey(child)}
childKey={getChildKey(child)}
isPresent
presenceAffectsLayout={presenceAffectsLayout}
mode={mode}
Expand Down Expand Up @@ -300,14 +308,15 @@ export const AnimatePresence: React.FunctionComponent<

// 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)) {
if (!(mode === "wait" && exiting.size)) {
childrenToRender = childrenToRender.concat(targetChunks[i])
}

if (child) {
childrenToRender.push(
<PresenceChild
key={key}
childKey={key}
isPresent
presenceAffectsLayout={presenceAffectsLayout}
mode={mode}
Expand All @@ -330,9 +339,9 @@ export const AnimatePresence: React.FunctionComponent<

return (
<>
{exitingChildren.size
{exiting.size
? childrenToRender
: childrenToRender.map((child) => cloneElement(child))}
</>
)
}
}

0 comments on commit 4348966

Please sign in to comment.