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/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..a9cafb3eed --- /dev/null +++ b/packages/react-native-gesture-handler/src/handlers/gestures/installUIRuntimeBindings.web.ts @@ -0,0 +1,5 @@ +export function installUIRuntimeBindings( + _getUIRuntimeHolder: (() => object) | undefined +) { + // noop +} 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..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; @@ -32,8 +31,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,59 +57,56 @@ 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; - -let uiRuntimeHolder: object | 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; try { // eslint-disable-next-line @typescript-eslint/no-var-requires - const Worklets = require('react-native-worklets') as WorkletsModule; - uiRuntimeHolder = Worklets?.getUIRuntimeHolder?.(); + Worklets = require('react-native-worklets') as WorkletsPackage; } 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 @@ -114,25 +119,8 @@ if (!Reanimated?.useSharedValue) { Reanimated = undefined; } -if (uiRuntimeHolder !== undefined || Reanimated !== undefined) { - ghQueueMicrotask(() => { - globalThis.__RNGH_UI_WORKLET_RUNTIME_HOLDER = uiRuntimeHolder; - - 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; - uiRuntimeHolder = undefined; - } - }); +if (Worklets !== undefined) { + installUIRuntimeBindings(Worklets.getUIRuntimeHolder); } if (Reanimated !== undefined && !Reanimated.setGestureState) { @@ -147,4 +135,4 @@ if (Reanimated !== undefined && !Reanimated.setGestureState) { }; } -export { Reanimated }; +export { Reanimated, Worklets }; 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..90693f32f7 --- /dev/null +++ b/packages/react-native-gesture-handler/src/v3/hooks/callbacks/lastUpdateEventMap.web.ts @@ -0,0 +1,7 @@ +import type { SimplifiedShareableHost } from '../../../handlers/gestures/reanimatedWrapper'; + +export type LastUpdateEventMap = Map; + +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 d7c72eab3b..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 @@ -12,6 +12,7 @@ import type { UnpackedGestureHandlerEventWithHandlerData, } from '../../types'; import { eventHandler } from './eventHandler'; +import { createLastUpdateEventMap } from './lastUpdateEventMap'; const REANIMATED_EVENT_NAMES = [ 'onGestureHandlerReanimatedEvent', @@ -24,12 +25,8 @@ const workletNOOP = () => { // no-op }; -function createLastUpdateEventMap() { - return Reanimated?.makeMutable(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 +34,8 @@ function getLastUpdateEventMap() { return lastUpdateEventMap; } +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 +43,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 +90,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;