From f6a78afc5b1c9f935130e0fce41b779e76c3a8cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20=C5=BBelawski?= Date: Fri, 7 Aug 2026 15:00:51 +0200 Subject: [PATCH 1/6] refactor: replace makeMutable with createShareable --- .../handlers/gestures/reanimatedWrapper.ts | 93 ++++++++++--------- .../callbacks/useReanimatedEventHandler.ts | 26 +++++- 2 files changed, 71 insertions(+), 48 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 8193c18917..e764fd151c 100644 --- a/packages/react-native-gesture-handler/src/handlers/gestures/reanimatedWrapper.ts +++ b/packages/react-native-gesture-handler/src/handlers/gestures/reanimatedWrapper.ts @@ -32,8 +32,17 @@ export type ReanimatedHandler = { context: ReanimatedContext; }; -type WorkletsModule = { +export type SimplifiedShareableHost = { + value: TValue; +}; + +type WorkletsPackage = { getUIRuntimeHolder?: () => object; + createShareable?: ( + hostRuntimeId: number, + initial: TValue + ) => SimplifiedShareableHost; + UIRuntimeId?: number; }; export type NativeEventsManager = new (component: { @@ -49,55 +58,53 @@ export type NativeEventsManager = new (component: { updateEvents: (prevProps: Record) => void; }; -let Reanimated: - | { - default: { - // Slightly modified definition copied from 'react-native-reanimated' - createAnimatedComponent

( - component: ComponentType

, - options?: unknown - ): ComponentClass

; - }; - NativeEventsManager: NativeEventsManager; - useHandler: ( - handlers: GestureCallbacks - ) => ReanimatedHandler; - useEvent: ( - callback: (event: T) => void, - events: string[], - rebuild: boolean - ) => (event: unknown) => void; - useSharedValue: (value: T) => SharedValue; - setGestureState: (handlerTag: number, newState: number) => void; - isSharedValue: (value: unknown) => value is SharedValue; - isWorkletFunction< - Args extends unknown[] = unknown[], - ReturnValue = unknown, - >( - value: unknown - ): value is WorkletFunction; - useComposedEventHandler( - handlers: (((event: T) => void) | null)[] - ): (event: T) => void; - // TODO: runOnJS and runOnUI are deprecated. These should be removed in near future. - runOnJS: ( - fn: (...args: A) => R - ) => (...args: Parameters) => void; - runOnUI( - fn: (...args: A) => R - ): (...args: Parameters) => void; - makeMutable(value: T): { value: T }; - } - | undefined; +type ReanimatedPackage = { + default: { + // Slightly modified definition copied from 'react-native-reanimated' + createAnimatedComponent

( + component: ComponentType

, + options?: unknown + ): ComponentClass

; + }; + NativeEventsManager: NativeEventsManager; + useHandler: ( + handlers: GestureCallbacks + ) => ReanimatedHandler; + useEvent: ( + callback: (event: T) => void, + events: string[], + rebuild: boolean + ) => (event: unknown) => void; + useSharedValue: (value: T) => SharedValue; + setGestureState: (handlerTag: number, newState: number) => void; + isSharedValue: (value: unknown) => value is SharedValue; + isWorkletFunction( + value: unknown + ): value is WorkletFunction; + useComposedEventHandler( + handlers: (((event: T) => void) | null)[] + ): (event: T) => void; + // TODO: runOnJS and runOnUI are deprecated. These should be removed in near future. + runOnJS: ( + fn: (...args: A) => R + ) => (...args: Parameters) => void; + runOnUI( + fn: (...args: A) => R + ): (...args: Parameters) => void; + makeMutable(value: T): { value: T }; +}; +let Reanimated: ReanimatedPackage | undefined; +let Worklets: WorkletsPackage | undefined; let uiRuntimeHolder: object | undefined; try { // eslint-disable-next-line @typescript-eslint/no-var-requires - const Worklets = require('react-native-worklets') as WorkletsModule; + Worklets = require('react-native-worklets') as WorkletsPackage; uiRuntimeHolder = Worklets?.getUIRuntimeHolder?.(); } catch (e) { // When 'react-native-worklets' is not available we want to quietly continue + Worklets = undefined; } try { @@ -147,4 +154,4 @@ if (Reanimated !== undefined && !Reanimated.setGestureState) { }; } -export { Reanimated }; +export { Reanimated, Worklets }; 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 d7c72eab3b..27e75049f2 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 @@ -4,7 +4,10 @@ import type { ReanimatedContext, ReanimatedHandler, } from '../../../handlers/gestures/reanimatedWrapper'; -import { Reanimated } from '../../../handlers/gestures/reanimatedWrapper'; +import { + Reanimated, + Worklets, +} from '../../../handlers/gestures/reanimatedWrapper'; import type { ChangeCalculatorType, GestureCallbacks, @@ -25,11 +28,21 @@ const workletNOOP = () => { }; function createLastUpdateEventMap() { - return Reanimated?.makeMutable(new Map>()); + if ( + Worklets?.createShareable === undefined || + Worklets.UIRuntimeId === undefined + ) { + return undefined; + } + + return Worklets.createShareable( + Worklets.UIRuntimeId, + new Map() + ); } // Created lazily instead of at module scope so importing this module doesn't -// call into Reanimated during module evaluation. +// call into Worklets during module evaluation. let lastUpdateEventMap: ReturnType; function getLastUpdateEventMap() { @@ -37,6 +50,9 @@ function getLastUpdateEventMap() { return lastUpdateEventMap; } +type LastUpdateEventMap = Map; +type ShareableLastUpdateEventMap = ReturnType; + // Takes the map as an argument on purpose: reading the lazy `let` from this // module-scope worklet would snapshot its value at module evaluation — before // the first `getLastUpdateEventMap()` call — so the UI-runtime copy would stay @@ -44,7 +60,7 @@ function getLastUpdateEventMap() { // arguments are serialized fresh on every call, so they always carry the // initialized map. function deleteHandlerEventEntry( - map: ReturnType, + map: ShareableLastUpdateEventMap, handlerTag: number ) { 'worklet'; @@ -91,7 +107,7 @@ export function useReanimatedEventHandler< > ) => { 'worklet'; - // Undefined only when Reanimated is absent — and then this callback is + // Undefined only when Worklets is absent — and then this callback is // never registered (`Reanimated?.useEvent` below short-circuits). if (updateEventMap === undefined) { return; From 6de658091642763e4d28d1514163ae275d871216 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20=C5=BBelawski?= Date: Fri, 7 Aug 2026 15:17:10 +0200 Subject: [PATCH 2/6] chore: fix types --- .../src/handlers/gestures/reanimatedWrapper.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 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 e764fd151c..fc40770c70 100644 --- a/packages/react-native-gesture-handler/src/handlers/gestures/reanimatedWrapper.ts +++ b/packages/react-native-gesture-handler/src/handlers/gestures/reanimatedWrapper.ts @@ -96,19 +96,18 @@ type ReanimatedPackage = { let Reanimated: ReanimatedPackage | undefined; let Worklets: WorkletsPackage | undefined; -let uiRuntimeHolder: object | undefined; try { // eslint-disable-next-line @typescript-eslint/no-var-requires Worklets = require('react-native-worklets') as WorkletsPackage; - uiRuntimeHolder = Worklets?.getUIRuntimeHolder?.(); } catch (e) { // When 'react-native-worklets' is not available we want to quietly continue Worklets = undefined; } try { - Reanimated = require('react-native-reanimated'); + // eslint-disable-next-line @typescript-eslint/no-var-requires + Reanimated = require('react-native-reanimated') as ReanimatedPackage; } catch (e) { // When 'react-native-reanimated' is not available we want to quietly continue // @ts-ignore TS demands the variable to be initialized @@ -121,9 +120,10 @@ if (!Reanimated?.useSharedValue) { Reanimated = undefined; } -if (uiRuntimeHolder !== undefined || Reanimated !== undefined) { +if (Worklets !== undefined) { ghQueueMicrotask(() => { - globalThis.__RNGH_UI_WORKLET_RUNTIME_HOLDER = uiRuntimeHolder; + globalThis.__RNGH_UI_WORKLET_RUNTIME_HOLDER = + Worklets?.getUIRuntimeHolder?.(); try { const decorated = NativeProxy.installUIRuntimeBindings(); @@ -137,7 +137,6 @@ if (uiRuntimeHolder !== undefined || Reanimated !== undefined) { } } finally { globalThis.__RNGH_UI_WORKLET_RUNTIME_HOLDER = undefined; - uiRuntimeHolder = undefined; } }); } From eb4f501ac9d32fa12395d99fca4e71c9c5866a99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20=C5=BBelawski?= Date: Fri, 7 Aug 2026 16:46:21 +0200 Subject: [PATCH 3/6] chore: clanker review --- .../src/__tests__/RuntimeBindings.test.ts | 12 ------------ .../v3/hooks/callbacks/useReanimatedEventHandler.ts | 3 ++- 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/packages/react-native-gesture-handler/src/__tests__/RuntimeBindings.test.ts b/packages/react-native-gesture-handler/src/__tests__/RuntimeBindings.test.ts index 0b8e98d593..73adc6e989 100644 --- a/packages/react-native-gesture-handler/src/__tests__/RuntimeBindings.test.ts +++ b/packages/react-native-gesture-handler/src/__tests__/RuntimeBindings.test.ts @@ -63,18 +63,6 @@ test('passes the Worklets runtime holder to native during installation', () => { expect(globalThis.__RNGH_UI_WORKLET_RUNTIME_HOLDER).toBeUndefined(); }); -test('installs legacy bindings when Reanimated is available without Worklets', () => { - const installUIRuntimeBindings = jest.fn(() => true); - - loadReanimatedWrapper( - { reanimated: { useSharedValue: true } }, - installUIRuntimeBindings - ); - - expect(installUIRuntimeBindings).toHaveBeenCalledTimes(1); - expect(globalThis.__RNGH_UI_WORKLET_RUNTIME_HOLDER).toBeUndefined(); -}); - test('does not install bindings when no runtime provider is available', () => { const installUIRuntimeBindings = jest.fn(() => true); 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 27e75049f2..c8168e5796 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 @@ -30,7 +30,8 @@ const workletNOOP = () => { function createLastUpdateEventMap() { if ( Worklets?.createShareable === undefined || - Worklets.UIRuntimeId === undefined + Worklets.UIRuntimeId === undefined || + Reanimated === undefined ) { return undefined; } From 2ecf57a872b0087a4b0715ee9d02fa87a57e8a95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20=C5=BBelawski?= Date: Fri, 7 Aug 2026 18:07:31 +0200 Subject: [PATCH 4/6] machen --- .../v3/hooks/callbacks/lastUpdateEventMap.ts | 21 +++++++++++++++ .../hooks/callbacks/lastUpdateEventMap.web.ts | 7 +++++ .../callbacks/useReanimatedEventHandler.ts | 27 ++++--------------- 3 files changed, 33 insertions(+), 22 deletions(-) create mode 100644 packages/react-native-gesture-handler/src/v3/hooks/callbacks/lastUpdateEventMap.ts create mode 100644 packages/react-native-gesture-handler/src/v3/hooks/callbacks/lastUpdateEventMap.web.ts diff --git a/packages/react-native-gesture-handler/src/v3/hooks/callbacks/lastUpdateEventMap.ts b/packages/react-native-gesture-handler/src/v3/hooks/callbacks/lastUpdateEventMap.ts new file mode 100644 index 0000000000..4bd67c0c4a --- /dev/null +++ b/packages/react-native-gesture-handler/src/v3/hooks/callbacks/lastUpdateEventMap.ts @@ -0,0 +1,21 @@ +import { + Reanimated, + Worklets, +} from '../../../handlers/gestures/reanimatedWrapper'; + +export type LastUpdateEventMap = Map; + +export function createLastUpdateEventMap() { + if ( + Worklets?.createShareable === undefined || + Worklets.UIRuntimeId === undefined || + Reanimated === undefined + ) { + return undefined; + } + + return Worklets.createShareable( + Worklets.UIRuntimeId, + new Map() + ); +} diff --git a/packages/react-native-gesture-handler/src/v3/hooks/callbacks/lastUpdateEventMap.web.ts b/packages/react-native-gesture-handler/src/v3/hooks/callbacks/lastUpdateEventMap.web.ts new file mode 100644 index 0000000000..7bdd6a924a --- /dev/null +++ b/packages/react-native-gesture-handler/src/v3/hooks/callbacks/lastUpdateEventMap.web.ts @@ -0,0 +1,7 @@ +export type LastUpdateEventMap = Map; + +// `createShareable` from react-native-worklets throws on web, so the +// last-update-event map is never created on this platform. +export function createLastUpdateEventMap() { + return undefined; +} 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 c8168e5796..044c705fd2 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 @@ -4,10 +4,7 @@ import type { ReanimatedContext, ReanimatedHandler, } from '../../../handlers/gestures/reanimatedWrapper'; -import { - Reanimated, - Worklets, -} from '../../../handlers/gestures/reanimatedWrapper'; +import { Reanimated } from '../../../handlers/gestures/reanimatedWrapper'; import type { ChangeCalculatorType, GestureCallbacks, @@ -15,6 +12,7 @@ import type { UnpackedGestureHandlerEventWithHandlerData, } from '../../types'; import { eventHandler } from './eventHandler'; +import { createLastUpdateEventMap } from './lastUpdateEventMap'; const REANIMATED_EVENT_NAMES = [ 'onGestureHandlerReanimatedEvent', @@ -27,21 +25,6 @@ const workletNOOP = () => { // no-op }; -function createLastUpdateEventMap() { - if ( - Worklets?.createShareable === undefined || - Worklets.UIRuntimeId === undefined || - Reanimated === undefined - ) { - return undefined; - } - - return Worklets.createShareable( - Worklets.UIRuntimeId, - new Map() - ); -} - // Created lazily instead of at module scope so importing this module doesn't // call into Worklets during module evaluation. let lastUpdateEventMap: ReturnType; @@ -51,7 +34,6 @@ function getLastUpdateEventMap() { return lastUpdateEventMap; } -type LastUpdateEventMap = Map; type ShareableLastUpdateEventMap = ReturnType; // Takes the map as an argument on purpose: reading the lazy `let` from this @@ -108,8 +90,9 @@ export function useReanimatedEventHandler< > ) => { 'worklet'; - // Undefined only when Worklets is absent — and then this callback is - // never registered (`Reanimated?.useEvent` below short-circuits). + // Undefined when Worklets is absent or on web — in the former case this + // callback is never registered (`Reanimated?.useEvent` below + // short-circuits). if (updateEventMap === undefined) { return; } From 8ac28ffadedcd88977afdb0dd19e54a51a62e251 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20=C5=BBelawski?= Date: Fri, 7 Aug 2026 18:34:03 +0200 Subject: [PATCH 5/6] szpont web --- .../gestures/installUIRuntimeBindings.ts | 25 +++++++++++++++++++ .../gestures/installUIRuntimeBindings.web.ts | 8 ++++++ .../handlers/gestures/reanimatedWrapper.ts | 22 ++-------------- .../hooks/callbacks/lastUpdateEventMap.web.ts | 8 +++--- .../callbacks/useReanimatedEventHandler.ts | 5 ++-- 5 files changed, 41 insertions(+), 27 deletions(-) create mode 100644 packages/react-native-gesture-handler/src/handlers/gestures/installUIRuntimeBindings.ts create mode 100644 packages/react-native-gesture-handler/src/handlers/gestures/installUIRuntimeBindings.web.ts diff --git a/packages/react-native-gesture-handler/src/handlers/gestures/installUIRuntimeBindings.ts b/packages/react-native-gesture-handler/src/handlers/gestures/installUIRuntimeBindings.ts new file mode 100644 index 0000000000..16ebc5c910 --- /dev/null +++ b/packages/react-native-gesture-handler/src/handlers/gestures/installUIRuntimeBindings.ts @@ -0,0 +1,25 @@ +import { ghQueueMicrotask } from '../../ghQueueMicrotask'; +import { tagMessage } from '../../utils'; +import { NativeProxy } from '../../v3/NativeProxy'; + +export function installUIRuntimeBindings( + getUIRuntimeHolder: (() => object) | undefined +) { + ghQueueMicrotask(() => { + globalThis.__RNGH_UI_WORKLET_RUNTIME_HOLDER = getUIRuntimeHolder?.(); + + try { + const decorated = NativeProxy.installUIRuntimeBindings(); + + if (!decorated) { + console.warn( + tagMessage( + 'Failed to install UI runtime bindings. Please report this at https://github.com/software-mansion/react-native-gesture-handler/issues.' + ) + ); + } + } finally { + globalThis.__RNGH_UI_WORKLET_RUNTIME_HOLDER = undefined; + } + }); +} diff --git a/packages/react-native-gesture-handler/src/handlers/gestures/installUIRuntimeBindings.web.ts b/packages/react-native-gesture-handler/src/handlers/gestures/installUIRuntimeBindings.web.ts new file mode 100644 index 0000000000..8bfe376f0d --- /dev/null +++ b/packages/react-native-gesture-handler/src/handlers/gestures/installUIRuntimeBindings.web.ts @@ -0,0 +1,8 @@ +// UI runtime bindings are a native-only concept — there is no separate UI +// worklet runtime on web, and `getUIRuntimeHolder` from react-native-worklets +// throws when called there. +export function installUIRuntimeBindings( + _getUIRuntimeHolder: (() => object) | undefined +) { + // no-op +} 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 fc40770c70..722b56c6cb 100644 --- a/packages/react-native-gesture-handler/src/handlers/gestures/reanimatedWrapper.ts +++ b/packages/react-native-gesture-handler/src/handlers/gestures/reanimatedWrapper.ts @@ -1,13 +1,12 @@ import type { ComponentClass, ComponentType } from 'react'; -import { ghQueueMicrotask } from '../../ghQueueMicrotask'; import { tagMessage } from '../../utils'; -import { NativeProxy } from '../../v3/NativeProxy'; import type { GestureCallbacks, GestureUpdateEventWithHandlerData, SharedValue, } from '../../v3/types'; +import { installUIRuntimeBindings } from './installUIRuntimeBindings'; export type ReanimatedContext = { lastUpdateEvent: GestureUpdateEventWithHandlerData | undefined; @@ -121,24 +120,7 @@ if (!Reanimated?.useSharedValue) { } if (Worklets !== undefined) { - ghQueueMicrotask(() => { - globalThis.__RNGH_UI_WORKLET_RUNTIME_HOLDER = - Worklets?.getUIRuntimeHolder?.(); - - try { - const decorated = NativeProxy.installUIRuntimeBindings(); - - if (!decorated) { - console.warn( - tagMessage( - 'Failed to install UI runtime bindings. Please report this at https://github.com/software-mansion/react-native-gesture-handler/issues.' - ) - ); - } - } finally { - globalThis.__RNGH_UI_WORKLET_RUNTIME_HOLDER = undefined; - } - }); + installUIRuntimeBindings(Worklets.getUIRuntimeHolder); } if (Reanimated !== undefined && !Reanimated.setGestureState) { diff --git a/packages/react-native-gesture-handler/src/v3/hooks/callbacks/lastUpdateEventMap.web.ts b/packages/react-native-gesture-handler/src/v3/hooks/callbacks/lastUpdateEventMap.web.ts index 7bdd6a924a..90693f32f7 100644 --- a/packages/react-native-gesture-handler/src/v3/hooks/callbacks/lastUpdateEventMap.web.ts +++ b/packages/react-native-gesture-handler/src/v3/hooks/callbacks/lastUpdateEventMap.web.ts @@ -1,7 +1,7 @@ +import type { SimplifiedShareableHost } from '../../../handlers/gestures/reanimatedWrapper'; + export type LastUpdateEventMap = Map; -// `createShareable` from react-native-worklets throws on web, so the -// last-update-event map is never created on this platform. -export function createLastUpdateEventMap() { - return undefined; +export function createLastUpdateEventMap(): SimplifiedShareableHost { + return { value: new Map() }; } 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 044c705fd2..4218a1c83a 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 @@ -90,9 +90,8 @@ export function useReanimatedEventHandler< > ) => { 'worklet'; - // Undefined when Worklets is absent or on web — in the former case this - // callback is never registered (`Reanimated?.useEvent` below - // short-circuits). + // Undefined only when Worklets is absent — and then this callback is + // never registered (`Reanimated?.useEvent` below short-circuits). if (updateEventMap === undefined) { return; } From c6255110c6be18df009acdc2ab3617509e37b714 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20=C5=BBelawski?= Date: Mon, 10 Aug 2026 09:47:29 +0200 Subject: [PATCH 6/6] chore: deslop --- .../src/handlers/gestures/installUIRuntimeBindings.web.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/react-native-gesture-handler/src/handlers/gestures/installUIRuntimeBindings.web.ts b/packages/react-native-gesture-handler/src/handlers/gestures/installUIRuntimeBindings.web.ts index 8bfe376f0d..a9cafb3eed 100644 --- a/packages/react-native-gesture-handler/src/handlers/gestures/installUIRuntimeBindings.web.ts +++ b/packages/react-native-gesture-handler/src/handlers/gestures/installUIRuntimeBindings.web.ts @@ -1,8 +1,5 @@ -// UI runtime bindings are a native-only concept — there is no separate UI -// worklet runtime on web, and `getUIRuntimeHolder` from react-native-worklets -// throws when called there. export function installUIRuntimeBindings( _getUIRuntimeHolder: (() => object) | undefined ) { - // no-op + // noop }