From 1b3ae6494a11d0760254cc95026e00af4eb0489a Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Wed, 24 Jul 2024 16:41:01 +0200 Subject: [PATCH 01/18] Adding failing test --- .../tests/animate-presence-switch-waapi.tsx | 31 ++++++++++ .../animate-presence-switch-waapi.ts | 60 +++++++++++++++++++ 2 files changed, 91 insertions(+) create mode 100644 dev/react/src/tests/animate-presence-switch-waapi.tsx create mode 100644 packages/framer-motion/cypress/integration/animate-presence-switch-waapi.ts diff --git a/dev/react/src/tests/animate-presence-switch-waapi.tsx b/dev/react/src/tests/animate-presence-switch-waapi.tsx new file mode 100644 index 0000000000..2e4e5a0bf3 --- /dev/null +++ b/dev/react/src/tests/animate-presence-switch-waapi.tsx @@ -0,0 +1,31 @@ +import { AnimatePresence, motion } from "framer-motion" +import { useState } from "react" + +export const App = () => { + const [state, setState] = useState(0) + + return ( + <> + + + + {state} + + + + ) +} diff --git a/packages/framer-motion/cypress/integration/animate-presence-switch-waapi.ts b/packages/framer-motion/cypress/integration/animate-presence-switch-waapi.ts new file mode 100644 index 0000000000..d21246fad7 --- /dev/null +++ b/packages/framer-motion/cypress/integration/animate-presence-switch-waapi.ts @@ -0,0 +1,60 @@ +describe("AnimatePresence with WAAPI animations", () => { + it("Interrupting exiting animation doesn't break exit", () => { + cy.visit("?test=animate-presence-switch-waapi") + .wait(50) + .get(".item") + .should((items: any) => { + expect(items.length).to.equal(1) + expect(items[0].textContent).to.equal("0") + }) + .get("#switch") + .trigger("click", 10, 10, { force: true }) + .wait(50) + .get(".item") + .should((items: any) => { + expect(items.length).to.equal(2) + expect(items[0].textContent).to.equal("0") + expect(items[1].textContent).to.equal("1") + }) + .wait(200) + .get(".item") + .should((items: any) => { + expect(items.length).to.equal(1) + expect(items[0].textContent).to.equal("1") + }) + .get("#switch") + .trigger("click", 10, 10, { force: true }) + .wait(20) + .get("#switch") + .trigger("click", 10, 10, { force: true }) + .wait(20) + .get("#switch") + .trigger("click", 10, 10, { force: true }) + .wait(300) + .get(".item") + .should((items: any) => { + expect(items.length).to.equal(1) + }) + // .wait(50) + // .get("#b") + // .should(([$a]: any) => { + // expectBbox($a, { + // top: 200, + // left: 100, + // width: 100, + // height: 100, + // }) + // }) + // .get("#c") + // .should(([$a]: any) => { + // expectBbox($a, { + // top: 300, + // left: 100, + // width: 100, + // height: 100, + // }) + // }) + // .trigger("click", 60, 60, { force: true }) + // .wait(100) + }) +}) From 06a7b7d3e095fdea41bdf1506cb8c9095da144d2 Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Wed, 24 Jul 2024 16:42:25 +0200 Subject: [PATCH 02/18] Tidy test --- .../animate-presence-switch-waapi.ts | 21 ------------------- 1 file changed, 21 deletions(-) diff --git a/packages/framer-motion/cypress/integration/animate-presence-switch-waapi.ts b/packages/framer-motion/cypress/integration/animate-presence-switch-waapi.ts index d21246fad7..4d64acd78b 100644 --- a/packages/framer-motion/cypress/integration/animate-presence-switch-waapi.ts +++ b/packages/framer-motion/cypress/integration/animate-presence-switch-waapi.ts @@ -35,26 +35,5 @@ describe("AnimatePresence with WAAPI animations", () => { .should((items: any) => { expect(items.length).to.equal(1) }) - // .wait(50) - // .get("#b") - // .should(([$a]: any) => { - // expectBbox($a, { - // top: 200, - // left: 100, - // width: 100, - // height: 100, - // }) - // }) - // .get("#c") - // .should(([$a]: any) => { - // expectBbox($a, { - // top: 300, - // left: 100, - // width: 100, - // height: 100, - // }) - // }) - // .trigger("click", 60, 60, { force: true }) - // .wait(100) }) }) From a0562365a8d3a703e65298cc99e8331ed7325fcb Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Wed, 24 Jul 2024 17:04:21 +0200 Subject: [PATCH 03/18] Fixing switching between components --- dev/react/src/tests/animate-presence-switch-waapi.tsx | 8 +++++++- .../cypress/integration/animate-presence-switch-waapi.ts | 1 + .../src/animation/animators/AcceleratedAnimation.ts | 3 +++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/dev/react/src/tests/animate-presence-switch-waapi.tsx b/dev/react/src/tests/animate-presence-switch-waapi.tsx index 2e4e5a0bf3..0f53d3e1e5 100644 --- a/dev/react/src/tests/animate-presence-switch-waapi.tsx +++ b/dev/react/src/tests/animate-presence-switch-waapi.tsx @@ -1,7 +1,8 @@ -import { AnimatePresence, motion } from "framer-motion" +import { AnimatePresence, motion, useMotionValue } from "framer-motion" import { useState } from "react" export const App = () => { + const count = useMotionValue(0) const [state, setState] = useState(0) return ( @@ -14,6 +15,10 @@ export const App = () => { > Switch +
+ Animation count:{" "} + {count} +
{ animate={{ opacity: 1 }} exit={{ opacity: 0 }} transition={{ duration: 0.2 }} + onAnimationStart={() => count.set(count.get() + 1)} > {state} diff --git a/packages/framer-motion/cypress/integration/animate-presence-switch-waapi.ts b/packages/framer-motion/cypress/integration/animate-presence-switch-waapi.ts index 4d64acd78b..9aebd3111a 100644 --- a/packages/framer-motion/cypress/integration/animate-presence-switch-waapi.ts +++ b/packages/framer-motion/cypress/integration/animate-presence-switch-waapi.ts @@ -34,6 +34,7 @@ describe("AnimatePresence with WAAPI animations", () => { .get(".item") .should((items: any) => { expect(items.length).to.equal(1) + expect(items[0].textContent).to.equal("0") }) }) }) diff --git a/packages/framer-motion/src/animation/animators/AcceleratedAnimation.ts b/packages/framer-motion/src/animation/animators/AcceleratedAnimation.ts index 05a443fe96..98843c7fe3 100644 --- a/packages/framer-motion/src/animation/animators/AcceleratedAnimation.ts +++ b/packages/framer-motion/src/animation/animators/AcceleratedAnimation.ts @@ -313,6 +313,9 @@ export class AcceleratedAnimation< this.isStopped = true if (this.state === "idle") return + this.resolveFinishedPromise() + this.updateFinishedPromise() + const { resolved } = this if (!resolved) return From 6439206804472153b840c4f2288ea4d7a6ec5738 Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Wed, 24 Jul 2024 18:44:24 +0200 Subject: [PATCH 04/18] Fixing double render --- .../tests/animate-presence-switch-waapi.tsx | 4 +- .../animate-presence-switch-waapi.ts | 20 +++ .../src/components/AnimatePresence/index.tsx | 118 ++++++++---------- 3 files changed, 77 insertions(+), 65 deletions(-) diff --git a/dev/react/src/tests/animate-presence-switch-waapi.tsx b/dev/react/src/tests/animate-presence-switch-waapi.tsx index 0f53d3e1e5..35a5ffa23a 100644 --- a/dev/react/src/tests/animate-presence-switch-waapi.tsx +++ b/dev/react/src/tests/animate-presence-switch-waapi.tsx @@ -16,11 +16,11 @@ export const App = () => { Switch
- Animation count:{" "} - {count} + Animation count: {count}
{ expect(items[0].textContent).to.equal("0") }) }) + + it("Interrupting exiting animation fire more animations than expected", () => { + cy.visit("?test=animate-presence-switch-waapi") + .wait(50) + .get(".item") + .should((items: any) => { + expect(items.length).to.equal(1) + expect(items[0].textContent).to.equal("0") + }) + .get("#switch") + .trigger("click", 10, 10, { force: true }) + .wait(20) + .get("#switch") + .trigger("click", 10, 10, { force: true }) + .wait(300) + .get("#count") + .should((count: any) => { + expect(count[0].textContent).to.equal("4") + }) + }) }) diff --git a/packages/framer-motion/src/components/AnimatePresence/index.tsx b/packages/framer-motion/src/components/AnimatePresence/index.tsx index 108c36f14c..e5cbafb7a0 100644 --- a/packages/framer-motion/src/components/AnimatePresence/index.tsx +++ b/packages/framer-motion/src/components/AnimatePresence/index.tsx @@ -1,7 +1,6 @@ import { useRef, isValidElement, - cloneElement, Children, ReactElement, ReactNode, @@ -173,71 +172,69 @@ 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) => { + exitingChildren.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) - let exitingComponent = component - if (!exitingComponent) { - const onExit = () => { - // clean up the exiting children map - exitingChildren.delete(key) - - // 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 - const leftOverKeys = Array.from(allChildren.keys()).filter( - (childKey) => !targetKeys.includes(childKey) - ) - - // clean up the all children map - leftOverKeys.forEach((leftOverKey) => - 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 - - forceRender() - onExitComplete && onExitComplete() - } - } + const onExit = () => { + // clean up the exiting children map + exitingChildren.delete(key) - exitingComponent = ( - - {child} - + // 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 + const leftOverKeys = Array.from(allChildren.keys()).filter( + (childKey) => !targetKeys.includes(childKey) ) - exitingChildren.set(key, exitingComponent) + + // clean up the all children map + leftOverKeys.forEach((leftOverKey) => + 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 + + forceRender() + onExitComplete && onExitComplete() + } } + const exitingComponent = ( + + {child} + + ) + exitingChildren.set(key, exitingComponent) + childrenToRender.splice(insertionIndex, 0, exitingComponent) }) @@ -245,8 +242,9 @@ export const AnimatePresence: React.FunctionComponent< // the same tree between renders childrenToRender = childrenToRender.map((child) => { const key = child.key as string | number - return exitingChildren.has(key) ? ( - child + + return exitingChildren.get(key) ? ( + exitingChildren.get(key)! ) : ( - {exitingChildren.size - ? childrenToRender - : childrenToRender.map((child) => cloneElement(child))} - - ) + return childrenToRender } From 825706fa23099c01c365528ad1f08c84957cfc05 Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Thu, 25 Jul 2024 09:32:02 +0200 Subject: [PATCH 05/18] Cleaning --- .../framer-motion/src/components/AnimatePresence/index.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/framer-motion/src/components/AnimatePresence/index.tsx b/packages/framer-motion/src/components/AnimatePresence/index.tsx index e5cbafb7a0..71f749cd8d 100644 --- a/packages/framer-motion/src/components/AnimatePresence/index.tsx +++ b/packages/framer-motion/src/components/AnimatePresence/index.tsx @@ -242,9 +242,10 @@ export const AnimatePresence: React.FunctionComponent< // the same tree between renders childrenToRender = childrenToRender.map((child) => { const key = child.key as string | number + const exitingChild = exitingChildren.get(key) - return exitingChildren.get(key) ? ( - exitingChildren.get(key)! + return exitingChild ? ( + exitingChild ) : ( Date: Thu, 25 Jul 2024 09:41:06 +0200 Subject: [PATCH 06/18] Replacing if --- .../src/components/AnimatePresence/index.tsx | 102 +++++++++--------- 1 file changed, 52 insertions(+), 50 deletions(-) diff --git a/packages/framer-motion/src/components/AnimatePresence/index.tsx b/packages/framer-motion/src/components/AnimatePresence/index.tsx index 71f749cd8d..e1fd2dd97e 100644 --- a/packages/framer-motion/src/components/AnimatePresence/index.tsx +++ b/packages/framer-motion/src/components/AnimatePresence/index.tsx @@ -172,7 +172,7 @@ 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((_, key) => { + exitingChildren.forEach((exitingComponent, key) => { // If this component is actually entering again, early return if (targetKeys.indexOf(key) !== -1) return @@ -182,58 +182,60 @@ export const AnimatePresence: React.FunctionComponent< const insertionIndex = presentKeys.indexOf(key) - const onExit = () => { - // clean up the exiting children map - exitingChildren.delete(key) - - // 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 - const leftOverKeys = Array.from(allChildren.keys()).filter( - (childKey) => !targetKeys.includes(childKey) - ) - - // clean up the all children map - leftOverKeys.forEach((leftOverKey) => - 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) - ) + if (!exitingComponent) { + const onExit = () => { + // clean up the exiting children map + exitingChildren.delete(key) + + // 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 + const leftOverKeys = Array.from(allChildren.keys()).filter( + (childKey) => !targetKeys.includes(childKey) + ) + + // clean up the all children map + leftOverKeys.forEach((leftOverKey) => + 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 + + forceRender() + onExitComplete && onExitComplete() } - ) - - // Defer re-rendering until all exiting children have indeed left - if (!exitingChildren.size) { - if (isMounted.current === false) return - - forceRender() - onExitComplete && onExitComplete() } - } - const exitingComponent = ( - - {child} - - ) - exitingChildren.set(key, exitingComponent) + exitingComponent = ( + + {child} + + ) + exitingChildren.set(key, exitingComponent) + } childrenToRender.splice(insertionIndex, 0, exitingComponent) }) From c6792cf0babfe9b82cde01d07aa6453cdcab3cbe Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Thu, 25 Jul 2024 09:42:03 +0200 Subject: [PATCH 07/18] Latest --- 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 e1fd2dd97e..289669c893 100644 --- a/packages/framer-motion/src/components/AnimatePresence/index.tsx +++ b/packages/framer-motion/src/components/AnimatePresence/index.tsx @@ -270,5 +270,5 @@ export const AnimatePresence: React.FunctionComponent< ) } - return childrenToRender + return <>{childrenToRender} } From a834bad57cb7cbf7f01a7002305e3796bd84d637 Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Thu, 25 Jul 2024 09:54:53 +0200 Subject: [PATCH 08/18] Fixing test --- dev/react/src/tests/animate-presence-pop.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev/react/src/tests/animate-presence-pop.tsx b/dev/react/src/tests/animate-presence-pop.tsx index 05b512114c..1a598ae04f 100644 --- a/dev/react/src/tests/animate-presence-pop.tsx +++ b/dev/react/src/tests/animate-presence-pop.tsx @@ -49,7 +49,7 @@ export const App = () => { opacity: 1, transition: { duration: 0.001 }, }} - exit={{ opacity: 0, transition: { duration: 10 } }} + exit={{ opacity: 0 }} layout style={{ ...itemStyle, backgroundColor: "green" }} /> From 19d515e977be5e1c6c46ea29b6e0a66b2222f300 Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Fri, 26 Jul 2024 11:03:44 +0200 Subject: [PATCH 09/18] Making concurrent safe --- .../src/examples/AnimatePresence-siblings.tsx | 19 +- .../src/examples/AnimatePresence-switch.tsx | 43 +++ dev/react/src/examples/AnimatePresence.tsx | 39 +- dev/react/src/tests/animate-presence-pop.tsx | 2 +- dev/react/src/tests/layout-exit.tsx | 2 +- .../src/components/AnimatePresence/index.tsx | 339 ++++++++---------- .../src/components/AnimatePresence/utils.ts | 27 ++ 7 files changed, 244 insertions(+), 227 deletions(-) create mode 100644 dev/react/src/examples/AnimatePresence-switch.tsx create mode 100644 packages/framer-motion/src/components/AnimatePresence/utils.ts diff --git a/dev/react/src/examples/AnimatePresence-siblings.tsx b/dev/react/src/examples/AnimatePresence-siblings.tsx index fa13956391..6dc42bfce3 100644 --- a/dev/react/src/examples/AnimatePresence-siblings.tsx +++ b/dev/react/src/examples/AnimatePresence-siblings.tsx @@ -17,16 +17,15 @@ const style = { function ExitComponent({ id }) { return ( - <> - - + ) } diff --git a/dev/react/src/examples/AnimatePresence-switch.tsx b/dev/react/src/examples/AnimatePresence-switch.tsx new file mode 100644 index 0000000000..12131f5617 --- /dev/null +++ b/dev/react/src/examples/AnimatePresence-switch.tsx @@ -0,0 +1,43 @@ +import { motion, AnimatePresence } from "framer-motion" +import { useState } from "react" + +/** + * An example of a single-child AnimatePresence animation + */ + +const style = { + width: 100, + height: 100, + background: "red", + opacity: 1, +} + +export const App = () => { + const [key, setKey] = useState("a") + + return ( +
{ + console.log("========= click =========") + setKey(key === "a" ? "b" : "a") + }} + > + console.log("rest")} + > + + +
+ ) +} diff --git a/dev/react/src/examples/AnimatePresence.tsx b/dev/react/src/examples/AnimatePresence.tsx index 1cb178902c..d65cbdcadb 100644 --- a/dev/react/src/examples/AnimatePresence.tsx +++ b/dev/react/src/examples/AnimatePresence.tsx @@ -1,5 +1,5 @@ import { motion, AnimatePresence } from "framer-motion" -import { useEffect, useState } from "react"; +import { useState } from "react" /** * An example of a single-child AnimatePresence animation @@ -15,26 +15,23 @@ const style = { export const App = () => { const [isVisible, setVisible] = useState(true) - useEffect(() => { - setTimeout(() => { - setVisible(!isVisible) - }, 1500) - }) - return ( - console.log("rest")} - > - {isVisible && ( - - )} - +
setVisible(!isVisible)}> + console.log("rest")} + > + {isVisible && ( + + )} + +
) } diff --git a/dev/react/src/tests/animate-presence-pop.tsx b/dev/react/src/tests/animate-presence-pop.tsx index 1a598ae04f..05b512114c 100644 --- a/dev/react/src/tests/animate-presence-pop.tsx +++ b/dev/react/src/tests/animate-presence-pop.tsx @@ -49,7 +49,7 @@ export const App = () => { opacity: 1, transition: { duration: 0.001 }, }} - exit={{ opacity: 0 }} + exit={{ opacity: 0, transition: { duration: 10 } }} layout style={{ ...itemStyle, backgroundColor: "green" }} /> diff --git a/dev/react/src/tests/layout-exit.tsx b/dev/react/src/tests/layout-exit.tsx index 388382fb98..826f8d0454 100644 --- a/dev/react/src/tests/layout-exit.tsx +++ b/dev/react/src/tests/layout-exit.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from "react"; +import { useState, useEffect } from "react" import { motion, AnimatePresence } from "framer-motion" export const App = () => { diff --git a/packages/framer-motion/src/components/AnimatePresence/index.tsx b/packages/framer-motion/src/components/AnimatePresence/index.tsx index 289669c893..7a0d7b3069 100644 --- a/packages/framer-motion/src/components/AnimatePresence/index.tsx +++ b/packages/framer-motion/src/components/AnimatePresence/index.tsx @@ -1,45 +1,12 @@ -import { - useRef, - isValidElement, - Children, - ReactElement, - ReactNode, - useContext, -} from "react" +import { useContext, useState, useMemo, useRef } from "react" import * as React from "react" 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 { invariant } from "../../utils/errors" - -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[] { - const filtered: ReactElement[] = [] - - // We use forEach here instead of map as map mutates the component key by preprending `.$` - Children.forEach(children, (child) => { - if (isValidElement(child)) filtered.push(child) - }) - - return filtered -} +import { useIsomorphicLayoutEffect } from "../../three-entry" +import { useConstant } from "../../utils/use-constant" +import { ComponentKey, arrayEquals, getChildKey, onlyElements } from "./utils" /** * `AnimatePresence` enables the animation of components that have been removed from the tree. @@ -78,197 +45,181 @@ export const AnimatePresence: React.FunctionComponent< React.PropsWithChildren > = ({ children, + exitBeforeEnter, custom, initial = true, onExitComplete, - exitBeforeEnter, presenceAffectsLayout = true, mode = "sync", }) => { invariant(!exitBeforeEnter, "Replace exitBeforeEnter with mode='wait'") - // We want to force a re-render once all exiting animations have finished. We - // either use a local forceRender function, or one from a parent context if it exists. - const forceRender = - useContext(LayoutGroupContext).forceRender || useForceUpdate()[0] - - 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 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>() - ).current - - // If this is the initial component render, just deal with logic surrounding whether - // we play onMount animations or not. + /** + * Filter any children that aren't ReactElements. We can only track components + * between renders with a props.key. + */ + const presentChildren = useMemo(() => onlyElements(children), [children]) + + /** + * Track the keys of the currently rendered children. This is used to + * determine which children are exiting. + */ + const presentKeys = presentChildren.map(getChildKey) + + /** + * If `initial={false}` we only want to pass this to components in the first render. + */ const isInitialRender = useRef(true) + /** + * A ref containing the currently present children. When all exit animations + * are complete, we use this to re-render the component with the latest children + * *committed* rather than the latest children *rendered*. + */ + const pendingPresentChildren = useRef(presentChildren) + + /** + * Track which exiting children have finished animating out. + */ + const exitComplete = useConstant(() => new Map()) + + /** + * Save children to render as React state. To ensure this component is concurrent-safe, + * we check for exiting children via an effect. + */ + const [diffedChildren, setDiffedChildren] = useState(presentChildren) + const [renderedChildren, setRenderedChildren] = useState(presentChildren) + useIsomorphicLayoutEffect(() => { isInitialRender.current = false + pendingPresentChildren.current = presentChildren - updateChildLookup(filteredChildren, allChildren) - presentChildren.current = childrenToRender - }) + /** + * Update complete status of exiting children. + */ + for (let i = 0; i < renderedChildren.length; i++) { + const key = getChildKey(renderedChildren[i]) - useUnmountEffect(() => { - isInitialRender.current = true - allChildren.clear() - exitingChildren.clear() - }) - - if (isInitialRender.current) { - return ( - <> - {childrenToRender.map((child) => ( - - {child} - - ))} - - ) - } - - // 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. - 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 - for (let i = 0; i < numPresent; i++) { - const key = presentKeys[i] - - if (targetKeys.indexOf(key) === -1 && !exitingChildren.has(key)) { - exitingChildren.set(key, undefined) + if (!presentKeys.includes(key)) { + if (exitComplete.get(key) !== true) { + exitComplete.set(key, false) + } + } else { + exitComplete.delete(key) + } } - } + }, [renderedChildren, presentKeys.length, presentKeys.join("-")]) - // 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 = [] - } - - // Loop through all currently exiting components and clone them to overwrite `animate` - // with any `exit` prop they might have defined. - exitingChildren.forEach((exitingComponent, 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 exitingChildren = [] - const insertionIndex = presentKeys.indexOf(key) + if (presentChildren !== diffedChildren) { + let nextChildren = [...presentChildren] - if (!exitingComponent) { - const onExit = () => { - // clean up the exiting children map - exitingChildren.delete(key) - - // 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 - const leftOverKeys = Array.from(allChildren.keys()).filter( - (childKey) => !targetKeys.includes(childKey) - ) - - // clean up the all children map - leftOverKeys.forEach((leftOverKey) => - allChildren.delete(leftOverKey) - ) + /** + * Loop through all the currently rendered components and decide which + * are exiting. + */ + for (let i = 0; i < renderedChildren.length; i++) { + const child = renderedChildren[i] + const key = getChildKey(child) - // make sure to render only the children that are actually visible - presentChildren.current = filteredChildren.filter( - (presentChild) => { - const presentChildKey = getChildKey(presentChild) + if (!presentKeys.includes(key)) { + nextChildren.splice(i, 0, child) + exitingChildren.push(child) + } + } - return ( - // filter out the node exiting - presentChildKey === key || - // filter out the leftover children - leftOverKeys.includes(presentChildKey) - ) - } - ) + /** + * If we're in "wait" mode, and we have exiting children, we want to + * only render these until they've all exited. + */ + if (mode === "wait" && exitingChildren.length) { + nextChildren = exitingChildren + } - // Defer re-rendering until all exiting children have indeed left - if (!exitingChildren.size) { - if (isMounted.current === false) return + nextChildren = onlyElements(nextChildren) - forceRender() - onExitComplete && onExitComplete() - } - } + const childrenHaveChanged = !arrayEquals( + nextChildren.map(getChildKey), + renderedChildren.map(getChildKey) + ) - exitingComponent = ( - - {child} - - ) - exitingChildren.set(key, exitingComponent) + if (childrenHaveChanged) { + setRenderedChildren(nextChildren) } - childrenToRender.splice(insertionIndex, 0, exitingComponent) - }) + setDiffedChildren(presentChildren) - // 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 - const exitingChild = exitingChildren.get(key) - - return exitingChild ? ( - exitingChild - ) : ( - - {child} - - ) - }) + /** + * Early return to ensure once we've set state with the latest diffed + * children, we can immediately re-render. + */ + return + } if ( process.env.NODE_ENV !== "production" && mode === "wait" && - childrenToRender.length > 1 + renderedChildren.length > 1 ) { console.warn( `You're attempting to animate multiple children within AnimatePresence, but its mode is set to "wait". This will lead to odd visual behaviour.` ) } - return <>{childrenToRender} + /** + * If we've been provided a forceRender function by the LayoutGroupContext, + * we can use it to force a re-render amongst all surrounding components once + * all components have finished animating out. + */ + const { forceRender } = useContext(LayoutGroupContext) + + return ( + <> + {renderedChildren.map((child) => { + const key = getChildKey(child) + + const isPresent = + presentChildren === renderedChildren || + presentKeys.includes(key) + + const onExit = () => { + if (exitComplete.has(key)) { + exitComplete.set(key, true) + } else { + return + } + + let isEveryExitComplete = true + exitComplete.forEach((isExitComplete) => { + if (!isExitComplete) isEveryExitComplete = false + }) + + if (isEveryExitComplete) { + forceRender?.() + setRenderedChildren(pendingPresentChildren.current) + onExitComplete && onExitComplete() + } + } + + return ( + + {child} + + ) + })} + + ) } diff --git a/packages/framer-motion/src/components/AnimatePresence/utils.ts b/packages/framer-motion/src/components/AnimatePresence/utils.ts new file mode 100644 index 0000000000..6e3630b347 --- /dev/null +++ b/packages/framer-motion/src/components/AnimatePresence/utils.ts @@ -0,0 +1,27 @@ +import { isValidElement, Children, ReactElement, ReactNode } from "react" + +export type ComponentKey = string | number + +export const getChildKey = (child: ReactElement): ComponentKey => + child.key || "" + +export function onlyElements(children: ReactNode): ReactElement[] { + const filtered: ReactElement[] = [] + + // We use forEach here instead of map as map mutates the component key by preprending `.$` + Children.forEach(children, (child) => { + if (isValidElement(child)) filtered.push(child) + }) + + return filtered +} + +export function arrayEquals(a: any[], b: any[]) { + if (a.length !== b.length) return false + + for (let i = 0; i < a.length; i++) { + if (a[i] !== b[i]) return false + } + + return true +} From c907c8a58056435cd60bfedf4d25f8585b5f48a9 Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Fri, 26 Jul 2024 13:27:24 +0200 Subject: [PATCH 10/18] Removing log --- dev/react/src/examples/AnimatePresence-switch.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/dev/react/src/examples/AnimatePresence-switch.tsx b/dev/react/src/examples/AnimatePresence-switch.tsx index 12131f5617..d9f03d33ad 100644 --- a/dev/react/src/examples/AnimatePresence-switch.tsx +++ b/dev/react/src/examples/AnimatePresence-switch.tsx @@ -18,7 +18,6 @@ export const App = () => { return (
{ - console.log("========= click =========") setKey(key === "a" ? "b" : "a") }} > From 77e67d13a0afa7bd51ac75d850fff065ba562ddd Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Fri, 26 Jul 2024 13:28:07 +0200 Subject: [PATCH 11/18] v11.3.18-alpha.0 --- dev/react/package.json | 4 ++-- lerna.json | 2 +- packages/framer-motion-3d/package.json | 4 ++-- packages/framer-motion/package.json | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/dev/react/package.json b/dev/react/package.json index 5dc6f33938..7d10b006ac 100644 --- a/dev/react/package.json +++ b/dev/react/package.json @@ -1,7 +1,7 @@ { "name": "react", "private": true, - "version": "11.3.17", + "version": "11.3.18-alpha.0", "type": "module", "scripts": { "dev": "vite", @@ -11,7 +11,7 @@ "preview": "vite preview" }, "dependencies": { - "framer-motion": "^11.3.17", + "framer-motion": "^11.3.18-alpha.0", "react": "^18.2.0", "react-dom": "^18.2.0" }, diff --git a/lerna.json b/lerna.json index 5ea20f1c80..e81d54bbc2 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "11.3.17", + "version": "11.3.18-alpha.0", "packages": [ "packages/*" ], diff --git a/packages/framer-motion-3d/package.json b/packages/framer-motion-3d/package.json index 43bfda1336..44b9e3e3ab 100644 --- a/packages/framer-motion-3d/package.json +++ b/packages/framer-motion-3d/package.json @@ -1,6 +1,6 @@ { "name": "framer-motion-3d", - "version": "11.3.17", + "version": "11.3.18-alpha.0", "description": "A simple and powerful React animation library for @react-three/fiber", "main": "dist/cjs/index.js", "module": "dist/es/index.mjs", @@ -47,7 +47,7 @@ "postpublish": "git push --tags" }, "dependencies": { - "framer-motion": "^11.3.17", + "framer-motion": "^11.3.18-alpha.0", "react-merge-refs": "^2.0.1" }, "peerDependencies": { diff --git a/packages/framer-motion/package.json b/packages/framer-motion/package.json index 4533bc6849..0b160e8fd2 100644 --- a/packages/framer-motion/package.json +++ b/packages/framer-motion/package.json @@ -1,6 +1,6 @@ { "name": "framer-motion", - "version": "11.3.17", + "version": "11.3.18-alpha.0", "description": "A simple and powerful JavaScript animation library", "main": "dist/cjs/index.js", "module": "dist/es/index.mjs", From f3c9de1ba4eb913f36e1f3e816f9c86784714910 Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Fri, 26 Jul 2024 13:28:46 +0200 Subject: [PATCH 12/18] Updating version --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 0e7a83180c..c5236056cb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7186,7 +7186,7 @@ __metadata: "@react-three/fiber": ^8.2.2 "@react-three/test-renderer": ^9.0.0 "@rollup/plugin-commonjs": ^22.0.1 - framer-motion: ^11.3.17 + framer-motion: ^11.3.18-alpha.0 react-merge-refs: ^2.0.1 three: ^0.137.0 peerDependencies: @@ -7197,7 +7197,7 @@ __metadata: languageName: unknown linkType: soft -"framer-motion@^11.3.17, framer-motion@workspace:packages/framer-motion": +"framer-motion@^11.3.18-alpha.0, framer-motion@workspace:packages/framer-motion": version: 0.0.0-use.local resolution: "framer-motion@workspace:packages/framer-motion" dependencies: @@ -12212,7 +12212,7 @@ __metadata: "@typescript-eslint/parser": ^7.2.0 "@vitejs/plugin-react-swc": ^3.5.0 eslint-plugin-react-refresh: ^0.4.6 - framer-motion: ^11.3.17 + framer-motion: ^11.3.18-alpha.0 react: ^18.2.0 react-dom: ^18.2.0 styled-components: ^6.1.11 From 6f35f6fe11a5ee2ca54a1fe8e343cb75166c1ef3 Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Fri, 26 Jul 2024 13:30:49 +0200 Subject: [PATCH 13/18] Publish --- package.json | 2 +- packages/framer-motion-3d/package.json | 2 +- packages/framer-motion/package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 9a1a0437d1..bf9a8f1a8a 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "test-e2e": "turbo run test-e2e", "test-ci": "turbo run test-ci --no-cache", "measure": "turbo run measure", - "prepare": "turbo run build measure test test-e2e", + "prepare": "", "new": "lerna publish from-package", "new-alpha": "turbo run build && lerna publish from-package --canary --preid alpha" }, diff --git a/packages/framer-motion-3d/package.json b/packages/framer-motion-3d/package.json index 44b9e3e3ab..3923198a0e 100644 --- a/packages/framer-motion-3d/package.json +++ b/packages/framer-motion-3d/package.json @@ -62,5 +62,5 @@ "@rollup/plugin-commonjs": "^22.0.1", "three": "^0.137.0" }, - "gitHead": "7ce78149a4f0587c409a660214131a04fca04c51" + "gitHead": "f3c9de1ba4eb913f36e1f3e816f9c86784714910" } diff --git a/packages/framer-motion/package.json b/packages/framer-motion/package.json index 0b160e8fd2..23ebd75bed 100644 --- a/packages/framer-motion/package.json +++ b/packages/framer-motion/package.json @@ -104,5 +104,5 @@ "maxSize": "18 kB" } ], - "gitHead": "7ce78149a4f0587c409a660214131a04fca04c51" + "gitHead": "f3c9de1ba4eb913f36e1f3e816f9c86784714910" } From 2b7c8ad979343e632d559768fe162c7107cc703e Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Fri, 26 Jul 2024 14:12:22 +0200 Subject: [PATCH 14/18] Updating animatepresence --- CHANGELOG.md | 6 ++++++ package.json | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bcca9c2b1f..60b4289a6c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ Framer Motion adheres to [Semantic Versioning](http://semver.org/). Undocumented APIs should be considered internal and may change without warning. +## [11.3.18] 2024-07-29 + +### Fixed + +- Improved correctness of `AnimatePresence` and made safe to use with concurrent rendering. + ## [11.3.17] 2024-07-24 ### Added diff --git a/package.json b/package.json index bf9a8f1a8a..9a1a0437d1 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "test-e2e": "turbo run test-e2e", "test-ci": "turbo run test-ci --no-cache", "measure": "turbo run measure", - "prepare": "", + "prepare": "turbo run build measure test test-e2e", "new": "lerna publish from-package", "new-alpha": "turbo run build && lerna publish from-package --canary --preid alpha" }, From f96f54bc59ce496437088c052fad1845a26a6013 Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Fri, 26 Jul 2024 14:16:38 +0200 Subject: [PATCH 15/18] Updating --- .../AnimatePresence/__tests__/AnimatePresence.test.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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..193a39c56d 100644 --- a/packages/framer-motion/src/components/AnimatePresence/__tests__/AnimatePresence.test.tsx +++ b/packages/framer-motion/src/components/AnimatePresence/__tests__/AnimatePresence.test.tsx @@ -153,7 +153,7 @@ describe("AnimatePresence", () => { }) test("when: afterChildren fires correctly", async () => { - const child = await new Promise((resolve) => { + const child = await new Promise(async (resolve) => { const parentOpacityOutput: ResolvedValues[] = [] const variants = { @@ -191,6 +191,7 @@ describe("AnimatePresence", () => { const { rerender } = render() rerender() + await nextFrame() rerender() rerender() }) @@ -419,7 +420,7 @@ describe("AnimatePresence", () => { // wait for the exit animation to check the DOM again setTimeout(() => { resolve(getByTestId("2").textContent === "2") - }, 150) + }, 200) }, 200) }) From e1c8e654c4af9945dff5e24f7a6bb21772913abf Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Fri, 26 Jul 2024 14:51:23 +0200 Subject: [PATCH 16/18] adding test file for wait --- .../src/examples/AnimatePresence-wait.tsx | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 dev/react/src/examples/AnimatePresence-wait.tsx diff --git a/dev/react/src/examples/AnimatePresence-wait.tsx b/dev/react/src/examples/AnimatePresence-wait.tsx new file mode 100644 index 0000000000..50629e0d9d --- /dev/null +++ b/dev/react/src/examples/AnimatePresence-wait.tsx @@ -0,0 +1,43 @@ +import { motion, AnimatePresence } from "framer-motion" +import { useState } from "react" + +/** + * An example of a single-child AnimatePresence animation + */ + +const style = { + width: 100, + height: 100, + background: "red", + opacity: 1, +} + +export const App = () => { + const [key, setKey] = useState(0) + + return ( +
{ + setKey(key + 1) + }} + > + console.log("rest")} + > + + +
+ ) +} From 0e121e668841891bc46d4b40149bfda82827606b Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Fri, 26 Jul 2024 15:01:40 +0200 Subject: [PATCH 17/18] Latest --- .../src/examples/AnimatePresence-variants.tsx | 2 +- .../__tests__/AnimatePresence.test.tsx | 17 ++++++++++------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/dev/react/src/examples/AnimatePresence-variants.tsx b/dev/react/src/examples/AnimatePresence-variants.tsx index ef67c0e0ea..9ae13eaa0d 100644 --- a/dev/react/src/examples/AnimatePresence-variants.tsx +++ b/dev/react/src/examples/AnimatePresence-variants.tsx @@ -1,5 +1,5 @@ import { motion, AnimatePresence } from "framer-motion" -import { useEffect, useState } from "react"; +import { useEffect, useState } from "react" /** * An example of AnimatePresence with exit defined as a variant through a tree. 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 193a39c56d..385c2679ed 100644 --- a/packages/framer-motion/src/components/AnimatePresence/__tests__/AnimatePresence.test.tsx +++ b/packages/framer-motion/src/components/AnimatePresence/__tests__/AnimatePresence.test.tsx @@ -192,6 +192,7 @@ describe("AnimatePresence", () => { const { rerender } = render() rerender() await nextFrame() + await nextFrame() rerender() rerender() }) @@ -398,6 +399,7 @@ describe("AnimatePresence", () => { return ( { rerender() setTimeout(() => { rerender() - }, 50) - setTimeout(() => { - rerender() - // wait for the exit animation to check the DOM again + setTimeout(() => { - resolve(getByTestId("2").textContent === "2") - }, 200) - }, 200) + rerender() + // wait for the exit animation to check the DOM again + setTimeout(() => { + resolve(getByTestId("2").textContent === "2") + }, 250) + }, 50) + }, 50) }) return await expect(promise).resolves.toBeTruthy() From fd7daeef05fc20f3b388335d37b5a8c68d932cf4 Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Fri, 26 Jul 2024 16:03:20 +0200 Subject: [PATCH 18/18] Updating --- .../src/animation/interfaces/visual-element.ts | 5 +---- .../src/components/AnimatePresence/index.tsx | 1 + .../framer-motion/src/gestures/__tests__/focus.test.tsx | 8 +++++--- .../framer-motion/src/gestures/__tests__/hover.test.tsx | 6 ++++-- .../framer-motion/src/motion/__tests__/variant.test.tsx | 4 +++- packages/framer-motion/src/render/VisualElement.ts | 1 + 6 files changed, 15 insertions(+), 10 deletions(-) diff --git a/packages/framer-motion/src/animation/interfaces/visual-element.ts b/packages/framer-motion/src/animation/interfaces/visual-element.ts index 591a8ad351..8aeca4977e 100644 --- a/packages/framer-motion/src/animation/interfaces/visual-element.ts +++ b/packages/framer-motion/src/animation/interfaces/visual-element.ts @@ -1,4 +1,3 @@ -import { frame } from "../../frameloop" import { resolveVariant } from "../../render/utils/resolve-dynamic-variants" import { VisualElement } from "../../render/VisualElement" import { AnimationDefinition } from "../types" @@ -33,8 +32,6 @@ export function animateVisualElement( } return animation.then(() => { - frame.postRender(() => { - visualElement.notify("AnimationComplete", definition) - }) + visualElement.notify("AnimationComplete", definition) }) } diff --git a/packages/framer-motion/src/components/AnimatePresence/index.tsx b/packages/framer-motion/src/components/AnimatePresence/index.tsx index 7a0d7b3069..7479795c12 100644 --- a/packages/framer-motion/src/components/AnimatePresence/index.tsx +++ b/packages/framer-motion/src/components/AnimatePresence/index.tsx @@ -198,6 +198,7 @@ export const AnimatePresence: React.FunctionComponent< if (isEveryExitComplete) { forceRender?.() setRenderedChildren(pendingPresentChildren.current) + onExitComplete && onExitComplete() } } diff --git a/packages/framer-motion/src/gestures/__tests__/focus.test.tsx b/packages/framer-motion/src/gestures/__tests__/focus.test.tsx index 340021857d..79694bd489 100644 --- a/packages/framer-motion/src/gestures/__tests__/focus.test.tsx +++ b/packages/framer-motion/src/gestures/__tests__/focus.test.tsx @@ -1,6 +1,6 @@ import { focus, blur, render } from "../../../jest.setup" -import { createRef } from "react"; -import { motion, motionValue } from "../../" +import { createRef } from "react" +import { frame, motion, motionValue } from "../../" import { nextFrame } from "./utils" describe("focus", () => { @@ -142,7 +142,9 @@ describe("focus", () => { const opacity = motionValue(1) let blurred = false - const onComplete = () => blurred && resolve(opacity.get()) + const onComplete = () => { + frame.postRender(() => blurred && resolve(opacity.get())) + } const Component = ({ onAnimationComplete }: any) => ( { const opacity = motionValue(1) let hasMousedOut = false - const onComplete = () => hasMousedOut && resolve(opacity.get()) + const onComplete = () => { + frame.postRender(() => hasMousedOut && resolve(opacity.get())) + } const Component = ({ onAnimationComplete }: any) => ( { }, } const display = motionValue("block") - const onComplete = () => resolve(display.get()) + const onComplete = () => { + frame.postRender(() => resolve(display.get())) + } const Component = () => (