From bdd3b6f55e0dfe6e3ecdb5363961f8d498877945 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82?= Date: Tue, 3 Nov 2026 11:17:10 +0100 Subject: [PATCH] Remove unused enabled listener from useJSResponderHandler --- .../src/__tests__/jsResponderHandler.test.tsx | 53 ++++++++++ .../src/v3/hooks/useJSResponderHandler.ts | 97 +++---------------- .../src/v3/hooks/utils/reanimatedUtils.ts | 21 ---- 3 files changed, 65 insertions(+), 106 deletions(-) create mode 100644 packages/react-native-gesture-handler/src/__tests__/jsResponderHandler.test.tsx diff --git a/packages/react-native-gesture-handler/src/__tests__/jsResponderHandler.test.tsx b/packages/react-native-gesture-handler/src/__tests__/jsResponderHandler.test.tsx new file mode 100644 index 0000000000..7ab2c6ce31 --- /dev/null +++ b/packages/react-native-gesture-handler/src/__tests__/jsResponderHandler.test.tsx @@ -0,0 +1,53 @@ +import { renderHook } from '@testing-library/react-native'; +import type { PropsWithChildren } from 'react'; +import React from 'react'; + +import { useJSResponderHandler } from '../v3/hooks/useJSResponderHandler'; +import { + JSResponderContext, + type JSResponderContextValue, +} from '../v3/scrollViewInterop'; +import { type Gesture, SingleGestureName } from '../v3/types'; + +jest.mock('../handlers/gestures/reanimatedWrapper', () => ({ + Reanimated: { + isSharedValue: (value: unknown) => + typeof value === 'object' && + value !== null && + '__isFakeSharedValue' in value, + }, +})); + +describe('useJSResponderHandler', () => { + test('reads a SharedValue `enabled` when the touch starts', () => { + const enabled = { __isFakeSharedValue: true, value: true }; + const gesture = { + type: SingleGestureName.Tap, + config: { enabled }, + } as unknown as Gesture; + const context: JSResponderContextValue = { + isRNGHResponderEvent: { current: false }, + keyboardShouldPersistTaps: 'handled', + }; + const wrapper = ({ children }: PropsWithChildren) => ( + {children} + ); + + const { result } = renderHook(() => useJSResponderHandler(gesture), { + wrapper, + }); + + // No re-render between the flips, like a SharedValue changed on the UI thread + const touch = () => { + context.isRNGHResponderEvent.current = false; + result.current.handleStartShouldSetResponder(); + return context.isRNGHResponderEvent.current; + }; + + expect(touch()).toBe(true); + enabled.value = false; + expect(touch()).toBe(false); + enabled.value = true; + expect(touch()).toBe(true); + }); +}); diff --git a/packages/react-native-gesture-handler/src/v3/hooks/useJSResponderHandler.ts b/packages/react-native-gesture-handler/src/v3/hooks/useJSResponderHandler.ts index 0230c3bdd8..1925f763d1 100644 --- a/packages/react-native-gesture-handler/src/v3/hooks/useJSResponderHandler.ts +++ b/packages/react-native-gesture-handler/src/v3/hooks/useJSResponderHandler.ts @@ -1,19 +1,13 @@ -import { use, useCallback, useEffect, useRef, useState } from 'react'; +import { use, useCallback } from 'react'; -import { Worklets } from '../../handlers/gestures/reanimatedWrapper'; import { JSResponderContext, updateResponderEventValue, } from '../scrollViewInterop'; -import { type Gesture, type SharedValue, SingleGestureName } from '../types'; +import { type Gesture, SingleGestureName } from '../types'; import { isComposedGesture, maybeUnpackValue } from './utils'; -import { - getEnabledSharedValues, - SHARED_VALUE_OFFSET, -} from './utils/reanimatedUtils'; -// adding 0.5 to not call Math.random and to make sure that listener ID is not an integer to avoid conflicts -let nextJSResponderContextListenerId = SHARED_VALUE_OFFSET + 0.5; +const ignoreResponderEvent = () => false; function isSupportedGesture< TConfig, @@ -57,90 +51,23 @@ export function useJSResponderHandler< countNativeAsHandled = true ) { const jsResponderContext = use(JSResponderContext); - const [enabledSharedValueRevision, setEnabledSharedValueRevision] = - useState(0); - const listenerIdRef = useRef(null); - - if (listenerIdRef.current === null) { - listenerIdRef.current = nextJSResponderContextListenerId++; - } - - useEffect(() => { - if (gesture === undefined) { - return; - } - - const enabledSharedValues = getEnabledSharedValues(gesture); - - if (Worklets === undefined || enabledSharedValues.length === 0) { - return; - } - - const listenerId = listenerIdRef.current; - if (listenerId === null) { - return; - } - - const { scheduleOnUI, scheduleOnRN } = Worklets; - - const notifyEnabledChanged = () => { - setEnabledSharedValueRevision((revision) => revision + 1); - }; - - const attachListeners = ( - sharedValues: SharedValue[], - id: number, - notify: () => void - ) => { - 'worklet'; - const listener = () => { - scheduleOnRN(notify); - }; - - for (const sharedValue of sharedValues) { - sharedValue.addListener(id, listener); - } - }; - - const detachListeners = ( - sharedValues: SharedValue[], - id: number - ) => { - 'worklet'; - for (const sharedValue of sharedValues) { - sharedValue.removeListener(id); - } - }; - - scheduleOnUI( - attachListeners, - enabledSharedValues, - listenerId, - notifyEnabledChanged - ); - - return () => { - scheduleOnUI(detachListeners, enabledSharedValues, listenerId); - }; - }, [gesture]); - - const shouldHandleJSResponderEvent = useCallback(() => { - void enabledSharedValueRevision; - return ( - gesture !== undefined && isSupportedGesture(gesture, countNativeAsHandled) - ); - }, [enabledSharedValueRevision, gesture, countNativeAsHandled]); + // `enabled` is read when the touch starts, so a SharedValue is always current const handleStartShouldSetResponder = useCallback(() => { - if (shouldHandleJSResponderEvent()) { + if ( + gesture !== undefined && + isSupportedGesture(gesture, countNativeAsHandled) + ) { updateResponderEventValue(jsResponderContext, true); } return false; - }, [jsResponderContext, shouldHandleJSResponderEvent]); + }, [jsResponderContext, gesture, countNativeAsHandled]); return { handleStartShouldSetResponder: - jsResponderContext == null ? () => false : handleStartShouldSetResponder, + jsResponderContext == null + ? ignoreResponderEvent + : handleStartShouldSetResponder, }; } diff --git a/packages/react-native-gesture-handler/src/v3/hooks/utils/reanimatedUtils.ts b/packages/react-native-gesture-handler/src/v3/hooks/utils/reanimatedUtils.ts index da22ceb543..50a56b6085 100644 --- a/packages/react-native-gesture-handler/src/v3/hooks/utils/reanimatedUtils.ts +++ b/packages/react-native-gesture-handler/src/v3/hooks/utils/reanimatedUtils.ts @@ -7,12 +7,10 @@ import { normalizeHitSlop } from '../../../handlers/hitSlop'; import { NativeProxy } from '../../NativeProxy'; import type { BaseGestureConfig, - Gesture, SharedValue, SharedValueOrT, } from '../../types'; import { HandlerCallbacks } from './propsWhiteList'; -import { isComposedGesture } from './relationUtils'; // Variant of djb2 hash function. // Taken from https://gist.github.com/eplawless/52813b1d8ad9af510d85?permalink_comment_id=3367765#gistcomment-3367765 @@ -169,22 +167,3 @@ export function maybeUnpackValue( return (Reanimated?.isSharedValue(v) ? v.value : v) as T; } - -export function getEnabledSharedValues< - TConfig, - THandlerData, - TExtendedHandlerData extends THandlerData, ->( - gesture: Gesture -): SharedValue[] { - if (Reanimated === undefined) { - return []; - } - - if (isComposedGesture(gesture)) { - return gesture.gestures.flatMap(getEnabledSharedValues); - } - - const enabled = gesture.config.enabled; - return Reanimated.isSharedValue(enabled) ? [enabled] : []; -}