From fdb77ae92444fa68128265254a11b3cd9950c33b Mon Sep 17 00:00:00 2001 From: Pieter De Baets Date: Mon, 26 Aug 2024 09:38:00 -0700 Subject: [PATCH] Reset animatedView when detaching AnimatedProps (#46205) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/46205 When React.Activity unmounts and remounts effects, we fail to re-attach the native view to the NativeAnimated nodes, which causes animations to stop working. Changelog: [Internal] Reviewed By: sammy-SC, bvanderhoof Differential Revision: D61662164 fbshipit-source-id: 8e86502f7258beba02d5e60b31864974d7288af5 --- .../Animated/__tests__/AnimatedNative-test.js | 55 ++++++++++++++++++- .../Libraries/Animated/nodes/AnimatedProps.js | 2 + 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/packages/react-native/Libraries/Animated/__tests__/AnimatedNative-test.js b/packages/react-native/Libraries/Animated/__tests__/AnimatedNative-test.js index 4d2f2ce3e32..6e5c9b302b8 100644 --- a/packages/react-native/Libraries/Animated/__tests__/AnimatedNative-test.js +++ b/packages/react-native/Libraries/Animated/__tests__/AnimatedNative-test.js @@ -25,6 +25,7 @@ jest import {format} from 'node:util'; import * as React from 'react'; +import {createRef} from 'react'; const {create, unmount, update} = require('../../../jest/renderer'); const Animated = require('../Animated').default; @@ -1265,7 +1266,7 @@ describe('Native Animated', () => { }); describe('Animated Components', () => { - it('Should restore default values on prop updates only', async () => { + it('should restore default values on prop updates only', async () => { const opacity = new Animated.Value(0); opacity.__makeNative(); @@ -1283,5 +1284,57 @@ describe('Native Animated', () => { 1, ); }); + + it('connects the native view on mount and disconnects on unmount', async () => { + const opacity = new Animated.Value(0, {useNativeDriver: true}); + + const root = await create(); + + // AnimatedProps > AnimatedStyle > opacity AnimatedValue + const propsTag = opacity + .__getChildren()[0] + .__getChildren()[0] + .__getNativeTag(); + expect(NativeAnimatedModule.connectAnimatedNodeToView).toBeCalledWith( + propsTag, + 1, + ); + + await unmount(root); + expect( + NativeAnimatedModule.disconnectAnimatedNodeFromView, + ).toBeCalledWith(propsTag, 1); + }); + + it('reconnects the native view when the component is remounted', async () => { + const opacity = new Animated.Value(0, {useNativeDriver: true}); + const ref = createRef(); + await create(); + + // AnimatedProps > AnimatedStyle > opacity AnimatedValue + const propsNode = opacity.__getChildren()[0].__getChildren()[0]; + let propsTag = propsNode.__nativeTag; + expect(NativeAnimatedModule.connectAnimatedNodeToView).nthCalledWith( + 1, + propsTag, + 1, + ); + + // Simulate what happens when React.Activity unmounts and remounts + propsNode.__detach(); + expect( + NativeAnimatedModule.disconnectAnimatedNodeFromView, + ).toBeCalledWith(propsTag, 1); + + propsNode.__attach(); + propsNode.setNativeView(ref.current); + + propsTag = propsNode.__nativeTag; + expect(NativeAnimatedModule.connectAnimatedNodeToView).nthCalledWith( + 2, + propsTag, + 1, + ); + }); }); }); diff --git a/packages/react-native/Libraries/Animated/nodes/AnimatedProps.js b/packages/react-native/Libraries/Animated/nodes/AnimatedProps.js index d6c3462590b..d8a048e82eb 100644 --- a/packages/react-native/Libraries/Animated/nodes/AnimatedProps.js +++ b/packages/react-native/Libraries/Animated/nodes/AnimatedProps.js @@ -88,6 +88,8 @@ export default class AnimatedProps extends AnimatedNode { if (this.__isNative && this._animatedView) { this.__disconnectAnimatedView(); } + this._animatedView = null; + for (const key in this._props) { const value = this._props[key]; if (value instanceof AnimatedNode) {