Skip to content

Commit

Permalink
fix: only intercept events with waiting handlers (#6739)
Browse files Browse the repository at this point in the history
<!-- Thanks for submitting a pull request! We appreciate you spending
the time to work on these changes. Please follow the template so that
the reviewers can easily understand what the code changes affect. -->

## Summary

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

## Test plan

See reproducer in #6684. Without this change, `onLayout` will not fire
on device rotation. With this change, `onLayout` will fire as expected.

---------

Co-authored-by: Krzysztof Piaskowy <[email protected]>
  • Loading branch information
mhoran and piaskowyk authored Feb 4, 2025
1 parent eef06b1 commit e8e14f0
Show file tree
Hide file tree
Showing 5 changed files with 84 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { runOnJS } from 'react-native-reanimated';

const notificationRegistry: Record<string, boolean> = {};
let notificationRegistry: Record<string, boolean> = {};
function notifyJS(name: string) {
notificationRegistry[name] = true;
}
Expand All @@ -25,4 +25,8 @@ export class NotificationRegistry {
}, 10);
});
}

public resetRegistry() {
notificationRegistry = {};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ export class TestRunner {

private async runTestCase(testSuite: TestSuite, testCase: TestCase) {
this._callTrackerRegistry.resetRegistry();
this._notificationRegistry.resetRegistry();
this._currentTestCase = testCase;

if (testSuite.beforeEach) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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');
},
},
{
Expand Down
Original file line number Diff line number Diff line change
@@ -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 }: { result: TestResult }) => {
const height = useSharedValue(styles.smallBox.height);

const onLayout = (event: LayoutChangeEvent) => {
result.height = event.nativeEvent.layout.height;
if (result.height === 200) {
notify('onLayout');
}
};

const animatedStyle = useAnimatedStyle(() => {
return { height: height.value };
});

useEffect(() => {
runOnUI(() => {
height.value += 100;
})();
}, [height]);

const setAnimatedHandlerCalled = () => {
result.animatedHandlerCalled = true;
notify('animatedOnLayout');
};

const animatedOnLayout = useEvent(() => {
'worklet';
runOnJS(setAnimatedHandlerCalled)();
}, ['onLayout']);

return (
<View onLayout={onLayout}>
<Animated.View style={[styles.smallBox, animatedStyle]} onLayout={animatedOnLayout} />
</View>
);
};

describe('onLayout', () => {
test('is not intercepted when there are no registered event handlers', async () => {
const result = {} as TestResult;
await render(<TestComponent result={result} />);
await Promise.race([waitForNotify('onLayout'), wait(1000)]);
expect(result.height).toBe(200);
});

test('is dispatched to the registered event handler', async () => {
const result = {} as TestResult;
await render(<TestComponent result={result} />);
await Promise.race([waitForNotify('animatedOnLayout'), wait(1000)]);
expect(result.animatedHandlerCalled).toBe(true);
});
});

const styles = StyleSheet.create({
smallBox: {
width: 100,
height: 100,
backgroundColor: 'pink',
},
});
Original file line number Diff line number Diff line change
Expand Up @@ -743,6 +743,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;
Expand Down

0 comments on commit e8e14f0

Please sign in to comment.