From 88f8b13e298678048283957d8537895b81c3049e Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Mon, 4 May 2026 12:31:05 +0200 Subject: [PATCH 1/2] Don't use context from useHandler --- .../handlers/gestures/reanimatedWrapper.ts | 1 + .../src/v3/hooks/callbacks/eventHandler.ts | 12 +++-------- .../callbacks/useReanimatedEventHandler.ts | 21 ++++++++++++++++--- 3 files changed, 22 insertions(+), 12 deletions(-) diff --git a/packages/react-native-gesture-handler/src/handlers/gestures/reanimatedWrapper.ts b/packages/react-native-gesture-handler/src/handlers/gestures/reanimatedWrapper.ts index 5708870405..403a37b487 100644 --- a/packages/react-native-gesture-handler/src/handlers/gestures/reanimatedWrapper.ts +++ b/packages/react-native-gesture-handler/src/handlers/gestures/reanimatedWrapper.ts @@ -77,6 +77,7 @@ let Reanimated: runOnUI( fn: (...args: A) => R ): (...args: Parameters) => void; + makeMutable(value: T): { value: T }; } | undefined; diff --git a/packages/react-native-gesture-handler/src/v3/hooks/callbacks/eventHandler.ts b/packages/react-native-gesture-handler/src/v3/hooks/callbacks/eventHandler.ts index 82480ffe68..f1c36f5202 100644 --- a/packages/react-native-gesture-handler/src/v3/hooks/callbacks/eventHandler.ts +++ b/packages/react-native-gesture-handler/src/v3/hooks/callbacks/eventHandler.ts @@ -3,7 +3,6 @@ import { CALLBACK_TYPE } from '../../../handlers/gestures/gesture'; import type { ReanimatedContext } from '../../../handlers/gestures/reanimatedWrapper'; import { State } from '../../../State'; import { TouchEventType } from '../../../TouchEventType'; -import { tagMessage } from '../../../utils'; import type { ChangeCalculatorType, GestureCallbacks, @@ -93,16 +92,11 @@ export function handleUpdateEvent< : eventWithData; const event = flattenAndFilterEvent(eventWithChanges); - - // This should never happen, but since we don't want to call hooks conditionally, we have to mark - // context as possibly undefined to make TypeScript happy. - if (!context) { - throw new Error(tagMessage('Event handler context is not defined')); - } - runCallback(CALLBACK_TYPE.UPDATE, handlers, event); - context.lastUpdateEvent = eventWithData; + if (context) { + context.lastUpdateEvent = eventWithData; + } } export function handleTouchEvent< diff --git a/packages/react-native-gesture-handler/src/v3/hooks/callbacks/useReanimatedEventHandler.ts b/packages/react-native-gesture-handler/src/v3/hooks/callbacks/useReanimatedEventHandler.ts index 76e98c4f70..023e7b41ad 100644 --- a/packages/react-native-gesture-handler/src/v3/hooks/callbacks/useReanimatedEventHandler.ts +++ b/packages/react-native-gesture-handler/src/v3/hooks/callbacks/useReanimatedEventHandler.ts @@ -1,6 +1,9 @@ import { useEffect, useMemo, useRef } from 'react'; -import type { ReanimatedHandler } from '../../../handlers/gestures/reanimatedWrapper'; +import type { + ReanimatedContext, + ReanimatedHandler, +} from '../../../handlers/gestures/reanimatedWrapper'; import { Reanimated } from '../../../handlers/gestures/reanimatedWrapper'; import type { ChangeCalculatorType, @@ -21,6 +24,10 @@ const workletNOOP = () => { // no-op }; +const lastUpdateEventMap = Reanimated?.makeMutable( + new Map>() +); + export function useReanimatedEventHandler< THandlerData, TExtendedHandlerData extends THandlerData, @@ -53,13 +60,21 @@ export function useReanimatedEventHandler< > ) => { 'worklet'; + // If we're on Reanimated path, lastUpdateEventMap should always be defined + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + let context = lastUpdateEventMap!.value.get(event.handlerTag); + if (context === undefined) { + context = { lastUpdateEvent: undefined }; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + lastUpdateEventMap!.value.set(event.handlerTag, context); + } + eventHandler( handlerTag, event, workletizedHandlers, changeEventCalculator, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion, @typescript-eslint/no-non-null-asserted-optional-chain - reanimatedHandler?.context!, + context as ReanimatedContext, false, fillInDefaultValues ); From 7ad5c9ddd37dd37482f04d66ff77cfadba6b76fc Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Mon, 4 May 2026 15:05:28 +0200 Subject: [PATCH 2/2] Clean up Co-authored-by: Copilot --- .../v3/hooks/callbacks/useReanimatedEventHandler.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/react-native-gesture-handler/src/v3/hooks/callbacks/useReanimatedEventHandler.ts b/packages/react-native-gesture-handler/src/v3/hooks/callbacks/useReanimatedEventHandler.ts index 023e7b41ad..279236eaa7 100644 --- a/packages/react-native-gesture-handler/src/v3/hooks/callbacks/useReanimatedEventHandler.ts +++ b/packages/react-native-gesture-handler/src/v3/hooks/callbacks/useReanimatedEventHandler.ts @@ -28,6 +28,12 @@ const lastUpdateEventMap = Reanimated?.makeMutable( new Map>() ); +function deleteHandlerEventEntry(handlerTag: number) { + 'worklet'; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + lastUpdateEventMap!.value.delete(handlerTag); +} + export function useReanimatedEventHandler< THandlerData, TExtendedHandlerData extends THandlerData, @@ -92,6 +98,10 @@ export function useReanimatedEventHandler< // ref from what was actually committed. useEffect(() => { prevHandlerTagRef.current = handlerTag; + + return () => { + Reanimated?.runOnUI?.(deleteHandlerEventEntry)(handlerTag); + }; }, [handlerTag]); const reanimatedEvent = Reanimated?.useEvent(