From 6784b78541948d18f0c437705402da5f6a4752d7 Mon Sep 17 00:00:00 2001 From: Matthew Horan Date: Wed, 20 Nov 2024 18:43:13 -0500 Subject: [PATCH] fix: only intercept events with waiting handlers Previously, NativeReanimatedModule::handleRawEvent would intercept all events received by the event listener. This resulted in an issue where onLayout would not fire in JS on the New Architecture. Instead, only intercept events with waiting handlers. This prevents asJSIValue from being called on the Reanimated event loop and allows onLayout to bubble up in JS. See https://github.com/facebook/react-native/blob/v0.76.2/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewEventEmitter.cpp#L82-L112, which prevents onLayout from being dispatched more than once. asJSIValue evaluates the lambda above in https://github.com/facebook/react-native/blob/v0.76.2/packages/react-native/ReactCommon/react/renderer/core/ValueFactoryEventPayload.cpp#L16. Fixes #6684 --- .../RuntimeTests/RuntimeTestsExample.tsx | 2 + .../RuntimeTests/tests/core/onLayout.test.tsx | 69 +++++++++++++++++++ .../NativeModules/NativeReanimatedModule.cpp | 5 ++ 3 files changed, 76 insertions(+) create mode 100644 apps/common-app/src/examples/RuntimeTests/tests/core/onLayout.test.tsx diff --git a/apps/common-app/src/examples/RuntimeTests/RuntimeTestsExample.tsx b/apps/common-app/src/examples/RuntimeTests/RuntimeTestsExample.tsx index 6a460dbf4ef..4ad23f2af2d 100644 --- a/apps/common-app/src/examples/RuntimeTests/RuntimeTestsExample.tsx +++ b/apps/common-app/src/examples/RuntimeTests/RuntimeTestsExample.tsx @@ -51,6 +51,8 @@ export default function RuntimeTestsExample() { require('./tests/core/useDerivedValue/chain.test'); require('./tests/core/useSharedValue/animationsCompilerApi.test'); + + require('./tests/core/onLayout.test'); }, }, { diff --git a/apps/common-app/src/examples/RuntimeTests/tests/core/onLayout.test.tsx b/apps/common-app/src/examples/RuntimeTests/tests/core/onLayout.test.tsx new file mode 100644 index 00000000000..7d74fecaf55 --- /dev/null +++ b/apps/common-app/src/examples/RuntimeTests/tests/core/onLayout.test.tsx @@ -0,0 +1,69 @@ +import { useEffect, useRef } from 'react'; +import type { LayoutChangeEvent } from 'react-native'; +import { StyleSheet, View } from 'react-native'; +import Animated, { runOnJS, runOnUI, useAnimatedStyle, useEvent, useSharedValue } from 'react-native-reanimated'; +import { describe, expect, render, test, wait } from '../../ReJest/RuntimeTestsApi'; + +interface TestResult { + height: number, + animatedHandlerCalled: boolean +} + +const TestComponent = ({result}: {result: TestResult}) => { + const sv = useSharedValue(styles.smallBox.height); + + const onLayout = (event: LayoutChangeEvent) => { + result.height = event.nativeEvent.layout.height; + }; + + const animatedStyle = useAnimatedStyle(() => { + return { height: sv.value }; + }); + + useEffect(() => { + runOnUI(() => { + sv.value += 100; + })(); + }); + + const setAnimatedHandlerCalled = () => { + result.animatedHandlerCalled = true; + }; + + const animatedOnLayout = useEvent(() => { + 'worklet'; + runOnJS(setAnimatedHandlerCalled)(); + }, ['onLayout']); + + return ( + + + + ); +}; + +describe('onLayout', () => { + test('is not intercepted when there are no registered event handlers', async () => { + const result = {} as TestResult; + await render(); + await wait(500); + const { height } = result; + expect(height).toBe(200); + }); + + test('is dispatched to the registered event handler', async () => { + const result = {} as TestResult; + await render(); + await wait(500); + const { animatedHandlerCalled } = result; + expect(animatedHandlerCalled).toBe(true); + }); +}); + +const styles = StyleSheet.create({ + smallBox: { + width: 100, + height: 100, + backgroundColor: 'pink', + }, +}); diff --git a/packages/react-native-reanimated/Common/cpp/reanimated/NativeModules/NativeReanimatedModule.cpp b/packages/react-native-reanimated/Common/cpp/reanimated/NativeModules/NativeReanimatedModule.cpp index 404ca6b6493..a6782f6f793 100644 --- a/packages/react-native-reanimated/Common/cpp/reanimated/NativeModules/NativeReanimatedModule.cpp +++ b/packages/react-native-reanimated/Common/cpp/reanimated/NativeModules/NativeReanimatedModule.cpp @@ -613,6 +613,11 @@ bool NativeReanimatedModule::handleRawEvent( if (eventType.rfind("top", 0) == 0) { eventType = "on" + eventType.substr(3); } + + if (!isAnyHandlerWaitingForEvent(eventType, tag)) { + return false; + } + jsi::Runtime &rt = uiWorkletRuntime_->getJSIRuntime(); const auto &eventPayload = rawEvent.eventPayload; jsi::Value payload = eventPayload->asJSIValue(rt);