From d8a240eadc1f6a28c02010c4fe4f9198ecdb57f5 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 | 71 +++++++++++++++++++ .../NativeModules/ReanimatedModuleProxy.cpp | 5 ++ 3 files changed, 78 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..bb138eccc5a --- /dev/null +++ b/apps/common-app/src/examples/RuntimeTests/tests/core/onLayout.test.tsx @@ -0,0 +1,71 @@ +import { useEffect } 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, notify, render, test, wait, waitForNotify } from '../../ReJest/RuntimeTestsApi'; + +interface TestResult { + height: number; + animatedHandlerCalled: boolean; +} + +const TestComponent = ({ result, notifyId }: { result: TestResult; notifyId: number }) => { + const height = useSharedValue(styles.smallBox.height); + + const onLayout = (event: LayoutChangeEvent) => { + result.height = event.nativeEvent.layout.height; + if (result.height === 200) { + notify(`onLayout${notifyId}`); + } + }; + + const animatedStyle = useAnimatedStyle(() => { + return { height: height.value }; + }); + + useEffect(() => { + runOnUI(() => { + height.value += 100; + })(); + }, [height]); + + const setAnimatedHandlerCalled = () => { + result.animatedHandlerCalled = true; + notify(`animatedOnLayout${notifyId}`); + }; + + 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 Promise.race([waitForNotify('onLayout1'), wait(1000)]); + expect(result.height).toBe(200); + }); + + test('is dispatched to the registered event handler', async () => { + const result = {} as TestResult; + await render(); + await Promise.race([waitForNotify('animatedOnLayout2'), wait(1000)]); + expect(result.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/ReanimatedModuleProxy.cpp b/packages/react-native-reanimated/Common/cpp/reanimated/NativeModules/ReanimatedModuleProxy.cpp index b159b070d6c..39599f6e45e 100644 --- a/packages/react-native-reanimated/Common/cpp/reanimated/NativeModules/ReanimatedModuleProxy.cpp +++ b/packages/react-native-reanimated/Common/cpp/reanimated/NativeModules/ReanimatedModuleProxy.cpp @@ -553,6 +553,11 @@ bool ReanimatedModuleProxy::handleRawEvent( if (eventType.rfind("top", 0) == 0) { eventType = "on" + eventType.substr(3); } + + if (!isAnyHandlerWaitingForEvent(eventType, tag)) { + return false; + } + jsi::Runtime &rt = workletsModuleProxy_->getUIWorkletRuntime()->getJSIRuntime(); const auto &eventPayload = rawEvent.eventPayload;