diff --git a/packages/react-native/Libraries/Animated/__tests__/AnimatedBackend-itest.js b/packages/react-native/Libraries/Animated/__tests__/AnimatedBackend-itest.js index 29eaab4df765..f581950bf029 100644 --- a/packages/react-native/Libraries/Animated/__tests__/AnimatedBackend-itest.js +++ b/packages/react-native/Libraries/Animated/__tests__/AnimatedBackend-itest.js @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @fantom_flags useSharedAnimatedBackend:true + * @fantom_flags useSharedAnimatedBackend:true updateRuntimeShadowNodeReferencesOnCommitThread:* * @flow strict-local * @format */ @@ -15,8 +15,8 @@ import type {HostInstance} from 'react-native'; import ensureInstance from '../../../src/private/__tests__/utilities/ensureInstance'; import * as Fantom from '@react-native/fantom'; -import {createRef, useEffect, useState} from 'react'; -import {Animated, useAnimatedValue} from 'react-native'; +import {createRef, memo, useEffect, useMemo, useState} from 'react'; +import {Animated, View, useAnimatedValue} from 'react-native'; import {allowStyleProp} from 'react-native/Libraries/Animated/NativeAnimatedAllowlist'; import ReactNativeElement from 'react-native/src/private/webapis/dom/nodes/ReactNativeElement'; @@ -468,3 +468,90 @@ test('animate width, height and opacity at once', () => { root.getRenderedOutput({props: ['width', 'height', 'opacity']}).toJSX(), ).toEqual(); }); + +test('animate width with memo and rerender (js sync test)', () => { + const viewRef = createRef(); + allowStyleProp('width'); + + let _widthAnimation; + let _setState; + + function useAnimation() { + const animatedValue = useAnimatedValue(100); + + useEffect(() => { + const animation = Animated.timing(animatedValue, { + toValue: 200, + duration: 1000, + useNativeDriver: true, + }); + _widthAnimation = animation; + animation.start(); + + return () => { + animation.stop(); + }; + }, [animatedValue]); + + return animatedValue; + } + + const AnimatedComponent = memo(() => { + const animatedValue = useAnimation(); + + const animatedStyle = useMemo(() => { + return { + width: animatedValue, + }; + }, [animatedValue]); + + return ( + + ); + }); + + function MyApp() { + const [state, setState] = useState(0); + _setState = setState; + + return ( + <> + + + + ); + } + + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render(); + }); + + expect(root.getRenderedOutput({props: ['width', 'height']}).toJSX()).toEqual( + , + ); + + Fantom.unstable_produceFramesForDuration(1000); + + // TODO: this shouldn't be necessary since animation should be stopped after duration + Fantom.runTask(() => { + _widthAnimation?.stop(); + }); + + expect(root.getRenderedOutput({props: ['width']}).toJSX()).toEqual( + , + ); + + // Trigger rerender after animation completes to see if animation state gets overwritten + Fantom.runTask(() => { + _setState(s => 1 - s); + }); + + expect(root.getRenderedOutput({props: ['width']}).toJSX()).toEqual( + , + ); +}); diff --git a/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.cpp b/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.cpp index 5bff2e5d119e..3ad43b8e0210 100644 --- a/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.cpp +++ b/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.cpp @@ -9,6 +9,7 @@ #include "AnimatedPropsRegistry.h" #include +#include #include #include #include @@ -152,8 +153,7 @@ void AnimationBackend::commitUpdates( return shadowNode.clone( {.props = newProps, .children = fragment.children, - .state = shadowNode.getState(), - .runtimeShadowNodeReference = false}); + .state = shadowNode.getState()}); })); }, {.mountSynchronously = true}); @@ -177,24 +177,45 @@ void AnimationBackend::requestAsyncFlushForSurfaces( react_native_assert( jsInvoker_ != nullptr || surfaces.empty() && "jsInvoker_ was not provided"); + std::weak_ptr weakAnimatedPropsRegistry = + animatedPropsRegistry_; for (const auto& surfaceId : surfaces) { // perform an empty commit on the js thread, to force the commit hook to // push updated shadow nodes to react through RSNRU - jsInvoker_->invokeAsync([weakUIManager = uiManager_, surfaceId]() { - auto uiManager = weakUIManager.lock(); - if (!uiManager) { - return; - } - uiManager->getShadowTreeRegistry().visit( - surfaceId, [](const ShadowTree& shadowTree) { - shadowTree.commit( - [](const RootShadowNode& oldRootShadowNode) { - return std::static_pointer_cast( - oldRootShadowNode.ShadowNode::clone({})); - }, - {.source = ShadowTreeCommitSource::AnimationEndSync}); - }); - }); + jsInvoker_->invokeAsync( + [weakUIManager = uiManager_, surfaceId, weakAnimatedPropsRegistry]() { + auto uiManager = weakUIManager.lock(); + if (!uiManager) { + return; + } + uiManager->getShadowTreeRegistry().visit( + surfaceId, + [weakAnimatedPropsRegistry](const ShadowTree& shadowTree) { + auto result = shadowTree.commit( + [weakAnimatedPropsRegistry]( + const RootShadowNode& oldRootShadowNode) { + return std::static_pointer_cast( + oldRootShadowNode.ShadowNode::clone({})); + }, + {.source = ShadowTreeCommitSource::AnimationEndSync}); + // To clear the registry, the updates neeed to be propagated to + // React with RSNRU. Without + // updateRuntimeShadowNodeReferencesOnCommitThread this won't + // happen if we do any commits on the main thread, since the + // runtimeShadowNodeReference_ is not propagated to nodes cloned + // outside of the JS thread. So when the flag is disabled we + // keep the updates in the registry and we will reapply them in + // a commit hook triggered by a rerender. + if (result == ShadowTree::CommitStatus::Succeeded && + ReactNativeFeatureFlags:: + updateRuntimeShadowNodeReferencesOnCommitThread()) { + if (auto animatedPropsRegistry = + weakAnimatedPropsRegistry.lock()) { + animatedPropsRegistry->clear(shadowTree.getSurfaceId()); + } + } + }); + }); } } diff --git a/private/react-native-fantom/tester/src/TesterAnimationChoreographer.cpp b/private/react-native-fantom/tester/src/TesterAnimationChoreographer.cpp index b9d32f853f1f..8c2c535911dd 100644 --- a/private/react-native-fantom/tester/src/TesterAnimationChoreographer.cpp +++ b/private/react-native-fantom/tester/src/TesterAnimationChoreographer.cpp @@ -7,6 +7,7 @@ #include "TesterAnimationChoreographer.h" #include +#include #include namespace facebook::react { @@ -20,7 +21,9 @@ void TesterAnimationChoreographer::pause() { void TesterAnimationChoreographer::runUITick(AnimationTimestamp timestamp) { if (!isPaused_) { + ShadowNode::setUseRuntimeShadowNodeReferenceUpdateOnThread(false); onAnimationFrame(timestamp); + ShadowNode::setUseRuntimeShadowNodeReferenceUpdateOnThread(true); } }