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);
}
}