From 828b8d9e52ee3db1693d5dad79a320f355e6e51f Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Fri, 23 Aug 2024 15:07:38 +0200 Subject: [PATCH] Removing double update listenrs on externall provided motion values (#2773) --- .../animate-stress-external-motion-value.tsx | 38 +++++++++++++++++++ .../src/animation/hooks/use-animated-state.ts | 25 ++++++------ .../motion/__tests__/animate-prop.test.tsx | 22 +++++++++++ .../framer-motion/src/render/VisualElement.ts | 6 ++- 4 files changed, 76 insertions(+), 15 deletions(-) create mode 100644 dev/react/src/examples/animate-stress-external-motion-value.tsx diff --git a/dev/react/src/examples/animate-stress-external-motion-value.tsx b/dev/react/src/examples/animate-stress-external-motion-value.tsx new file mode 100644 index 0000000000..424803ac6c --- /dev/null +++ b/dev/react/src/examples/animate-stress-external-motion-value.tsx @@ -0,0 +1,38 @@ +import { motion, useMotionValue } from "framer-motion" + +function Box({ i }: { i: number }) { + const rotate = useMotionValue(0) + + return ( + + ) +} + +export const App = () => { + const boxes = Array.from(Array(1000).keys()).map((i) => ( + + )) + + return ( +
+ {boxes} +
+ ) +} diff --git a/packages/framer-motion/src/animation/hooks/use-animated-state.ts b/packages/framer-motion/src/animation/hooks/use-animated-state.ts index 9cd2038d91..01133fb44c 100644 --- a/packages/framer-motion/src/animation/hooks/use-animated-state.ts +++ b/packages/framer-motion/src/animation/hooks/use-animated-state.ts @@ -1,4 +1,4 @@ -import { useEffect, useState } from "react" +import { useState, useLayoutEffect } from "react" import { useConstant } from "../../utils/use-constant" import { TargetAndTransition } from "../../types" import { ResolvedValues } from "../../render/types" @@ -60,27 +60,24 @@ export function useAnimatedState(initialState: any) { const element = useConstant(() => { return new StateVisualElement( - { props: {}, visualState, presenceContext: null }, + { + props: { + onUpdate: (v) => { + setAnimationState({ ...v }) + }, + }, + visualState, + presenceContext: null, + }, { initialState } ) }) - useEffect(() => { + useLayoutEffect(() => { element.mount({}) return () => element.unmount() }, [element]) - useEffect(() => { - element.update( - { - onUpdate: (v) => { - setAnimationState({ ...v }) - }, - }, - null - ) - }, [setAnimationState, element]) - const startAnimation = useConstant( () => (animationDefinition: TargetAndTransition) => { return animateVisualElement(element, animationDefinition) diff --git a/packages/framer-motion/src/motion/__tests__/animate-prop.test.tsx b/packages/framer-motion/src/motion/__tests__/animate-prop.test.tsx index d84bd6f843..77cdc47dd0 100644 --- a/packages/framer-motion/src/motion/__tests__/animate-prop.test.tsx +++ b/packages/framer-motion/src/motion/__tests__/animate-prop.test.tsx @@ -1242,4 +1242,26 @@ describe("animate prop as object", () => { return expect(result.length).not.toBe(1) }) + + test("Doesn't double-add listeners to externally-provided motion values", async () => { + const result = await new Promise((resolve) => { + const Component = () => { + const x = useMotionValue(0) + return ( + + resolve((x as any).events.change.getSize()) + } + style={{ x }} + /> + ) + } + const { rerender } = render() + rerender() + }) + + return expect(result).toBe(1) + }) }) diff --git a/packages/framer-motion/src/render/VisualElement.ts b/packages/framer-motion/src/render/VisualElement.ts index 32ef9e3c42..df7d069c9c 100644 --- a/packages/framer-motion/src/render/VisualElement.ts +++ b/packages/framer-motion/src/render/VisualElement.ts @@ -445,6 +445,10 @@ export abstract class VisualElement< } private bindToMotionValue(key: string, value: MotionValue) { + if (this.valueSubscriptions.has(key)) { + this.valueSubscriptions.get(key)!() + } + const valueIsTransform = transformProps.has(key) const removeOnChange = value.on( @@ -508,7 +512,7 @@ export abstract class VisualElement< FeatureConstructor && isEnabled(this.props) ) { - this.features[key] = new FeatureConstructor(this) + this.features[key] = new FeatureConstructor(this) as any } /**