diff --git a/src/createAnimatedComponent/PropsFilter.tsx b/src/createAnimatedComponent/PropsFilter.tsx index eb0ae1b399e7..d7f5a3790ad9 100644 --- a/src/createAnimatedComponent/PropsFilter.tsx +++ b/src/createAnimatedComponent/PropsFilter.tsx @@ -4,7 +4,7 @@ import { shallowEqual } from '../reanimated2/hook/utils'; import type { StyleProps } from '../reanimated2/commonTypes'; import { isSharedValue } from '../reanimated2/isSharedValue'; import { isChromeDebugger } from '../reanimated2/PlatformChecker'; -import WorkletEventHandler from '../reanimated2/WorkletEventHandler'; +import { WorkletEventHandler } from '../reanimated2/WorkletEventHandler'; import { initialUpdaterRun } from '../reanimated2/animation'; import { hasInlineStyles, getInlineStyle } from './InlinePropManager'; import type { diff --git a/src/createAnimatedComponent/createAnimatedComponent.tsx b/src/createAnimatedComponent/createAnimatedComponent.tsx index 19fb267e1cef..df9687cfced8 100644 --- a/src/createAnimatedComponent/createAnimatedComponent.tsx +++ b/src/createAnimatedComponent/createAnimatedComponent.tsx @@ -8,7 +8,7 @@ import type { } from 'react'; import React from 'react'; import { findNodeHandle, Platform } from 'react-native'; -import WorkletEventHandler from '../reanimated2/WorkletEventHandler'; +import { WorkletEventHandler } from '../reanimated2/WorkletEventHandler'; import '../reanimated2/layoutReanimation/animationsManager'; import invariant from 'invariant'; import { adaptViewConfig } from '../ConfigHelper'; @@ -143,6 +143,7 @@ export function createAnimatedComponent( } componentDidMount() { + this._viewTag = this._getViewInfo().viewTag as number; this._attachNativeEvents(); this._jsPropsUpdater.addOnJSPropsChangeListener(this); this._attachAnimatedStyles(); @@ -228,21 +229,13 @@ export function createAnimatedComponent( } _attachNativeEvents() { - const node = this._getEventViewRef() as AnimatedComponentRef; - let viewTag = null; // We set it only if needed - for (const key in this.props) { const prop = this.props[key]; if ( has('workletEventHandler', prop) && prop.workletEventHandler instanceof WorkletEventHandler ) { - if (viewTag === null) { - viewTag = IS_WEB - ? this._component - : findNodeHandle(options?.setNativeProps ? this : node); - } - prop.workletEventHandler.registerForEvents(viewTag as number, key); + prop.workletEventHandler.registerForEvents(this._viewTag, key); } } } @@ -254,7 +247,7 @@ export function createAnimatedComponent( has('workletEventHandler', prop) && prop.workletEventHandler instanceof WorkletEventHandler ) { - prop.workletEventHandler.unregisterFromEvents(); + prop.workletEventHandler.unregisterFromEvents(this._viewTag); } } } @@ -277,38 +270,40 @@ export function createAnimatedComponent( } } - _reattachNativeEvents( + _updateNativeEvents( prevProps: AnimatedComponentProps ) { for (const key in prevProps) { - const prop = this.props[key]; + const prevProp = prevProps[key]; if ( - has('workletEventHandler', prop) && - prop.workletEventHandler instanceof WorkletEventHandler && - prop.workletEventHandler.reattachNeeded + has('workletEventHandler', prevProp) && + prevProp.workletEventHandler instanceof WorkletEventHandler ) { - prop.workletEventHandler.unregisterFromEvents(); + const newProp = this.props[key]; + if (!newProp) { + // Prop got deleted + prevProp.workletEventHandler.unregisterFromEvents(this._viewTag); + } else if ( + has('workletEventHandler', newProp) && + newProp.workletEventHandler instanceof WorkletEventHandler && + newProp.workletEventHandler !== prevProp.workletEventHandler + ) { + // Prop got changed + prevProp.workletEventHandler.unregisterFromEvents(this._viewTag); + newProp.workletEventHandler.registerForEvents(this._viewTag); + } } } - let viewTag = null; - for (const key in this.props) { - const prop = this.props[key]; + const newProp = this.props[key]; if ( - has('workletEventHandler', prop) && - prop.workletEventHandler instanceof WorkletEventHandler && - prop.workletEventHandler.reattachNeeded + has('workletEventHandler', newProp) && + newProp.workletEventHandler instanceof WorkletEventHandler && + !prevProps[key] ) { - if (viewTag === null) { - const node = this._getEventViewRef() as AnimatedComponentRef; - - viewTag = IS_WEB - ? this._component - : findNodeHandle(options?.setNativeProps ? this : node); - } - prop.workletEventHandler.registerForEvents(viewTag as number, key); - prop.workletEventHandler.reattachNeeded = false; + // Prop got added + newProp.workletEventHandler.registerForEvents(this._viewTag); } } } @@ -460,7 +455,7 @@ export function createAnimatedComponent( ) { this._configureSharedTransition(); } - this._reattachNativeEvents(prevProps); + this._updateNativeEvents(prevProps); this._attachAnimatedStyles(); this._InlinePropManager.attachInlineProps(this, this._getViewInfo()); diff --git a/src/reanimated2/WorkletEventHandler.ts b/src/reanimated2/WorkletEventHandler.ts index 6b233132a5c9..439efaad7694 100644 --- a/src/reanimated2/WorkletEventHandler.ts +++ b/src/reanimated2/WorkletEventHandler.ts @@ -1,7 +1,11 @@ 'use strict'; import type { NativeSyntheticEvent } from 'react-native'; import { registerEventHandler, unregisterEventHandler } from './core'; -import type { EventPayload, ReanimatedEvent } from './hook/commonTypes'; +import type { + EventPayload, + ReanimatedEvent, + IWorkletEventHandler, +} from './hook/commonTypes'; import { shouldBeUseWeb } from './PlatformChecker'; const SHOULD_BE_USE_WEB = shouldBeUseWeb(); @@ -20,64 +24,119 @@ function jsListener( }; } -export default class WorkletEventHandler { +class WorkletEventHandlerNative + implements IWorkletEventHandler +{ + eventNames: string[]; worklet: (event: ReanimatedEvent) => void; + #viewTags: Set; + #registrations: Map; // keys are viewTags, values are arrays of registration ID's for each viewTag + constructor( + worklet: (event: ReanimatedEvent) => void, + eventNames: string[] + ) { + this.worklet = worklet; + this.eventNames = eventNames; + this.#viewTags = new Set(); + this.#registrations = new Map(); + } + + updateEventHandler( + newWorklet: (event: ReanimatedEvent) => void, + newEvents: string[] + ): void { + // Update worklet and event names + this.worklet = newWorklet; + this.eventNames = newEvents; + + // Detach all events + this.#registrations.forEach((registrationIDs) => { + registrationIDs.forEach((id) => unregisterEventHandler(id)); + // No need to remove registrationIDs from map, since it gets overwritten when attaching + }); + + // Attach new events with new worklet + Array.from(this.#viewTags).forEach((tag) => { + const newRegistrations = this.eventNames.map((eventName) => + registerEventHandler(this.worklet, eventName, tag) + ); + this.#registrations.set(tag, newRegistrations); + }); + } + + registerForEvents(viewTag: number, fallbackEventName?: string): void { + this.#viewTags.add(viewTag); + + const newRegistrations = this.eventNames.map((eventName) => + registerEventHandler(this.worklet, eventName, viewTag) + ); + this.#registrations.set(viewTag, newRegistrations); + + if (this.eventNames.length === 0 && fallbackEventName) { + const newRegistration = registerEventHandler( + this.worklet, + fallbackEventName, + viewTag + ); + this.#registrations.set(viewTag, [newRegistration]); + } + } + + unregisterFromEvents(viewTag: number): void { + this.#viewTags.delete(viewTag); + this.#registrations.get(viewTag)?.forEach((id) => { + unregisterEventHandler(id); + }); + this.#registrations.delete(viewTag); + } +} + +class WorkletEventHandlerWeb + implements IWorkletEventHandler +{ eventNames: string[]; - reattachNeeded: boolean; listeners: | Record>) => void> | Record) => void>; - viewTag: number | undefined; - registrations: number[]; + worklet: (event: ReanimatedEvent) => void; + constructor( worklet: (event: ReanimatedEvent) => void, eventNames: string[] = [] ) { this.worklet = worklet; this.eventNames = eventNames; - this.reattachNeeded = false; this.listeners = {}; - this.viewTag = undefined; - this.registrations = []; - - if (SHOULD_BE_USE_WEB) { - this.listeners = eventNames.reduce( - ( - acc: Record) => void>, - eventName: string - ) => { - acc[eventName] = jsListener(eventName, worklet); - return acc; - }, - {} - ); - } + this.setupWebListeners(); } - updateWorklet(newWorklet: (event: ReanimatedEvent) => void): void { - this.worklet = newWorklet; - this.reattachNeeded = true; + setupWebListeners() { + this.listeners = {}; + this.eventNames.forEach((eventName) => { + this.listeners[eventName] = jsListener(eventName, this.worklet); + }); } - registerForEvents(viewTag: number, fallbackEventName?: string): void { - this.viewTag = viewTag; - this.registrations = this.eventNames.map((eventName) => - registerEventHandler(this.worklet, eventName, viewTag) - ); - if (this.registrations.length === 0 && fallbackEventName) { - this.registrations.push( - registerEventHandler(this.worklet, fallbackEventName, viewTag) - ); - } + updateEventHandler( + newWorklet: (event: ReanimatedEvent) => void, + newEvents: string[] + ): void { + // Update worklet and event names + this.worklet = newWorklet; + this.eventNames = newEvents; + this.setupWebListeners(); } - registerForEventByName(eventName: string) { - this.registrations.push(registerEventHandler(this.worklet, eventName)); + registerForEvents(_viewTag: number, _fallbackEventName?: string): void { + // noop } - unregisterFromEvents(): void { - this.registrations.forEach((id) => unregisterEventHandler(id)); - this.registrations = []; + unregisterFromEvents(_viewTag: number): void { + // noop } } + +export const WorkletEventHandler = SHOULD_BE_USE_WEB + ? WorkletEventHandlerWeb + : WorkletEventHandlerNative; diff --git a/src/reanimated2/hook/commonTypes.ts b/src/reanimated2/hook/commonTypes.ts index 573573edd864..c7fb9879bc5f 100644 --- a/src/reanimated2/hook/commonTypes.ts +++ b/src/reanimated2/hook/commonTypes.ts @@ -79,6 +79,15 @@ export type RNNativeScrollEvent = NativeSyntheticEvent; export type ReanimatedScrollEvent = ReanimatedEvent; +export interface IWorkletEventHandler { + updateEventHandler: ( + newWorklet: (event: ReanimatedEvent) => void, + newEvents: string[] + ) => void; + registerForEvents: (viewTag: number, fallbackEventName?: string) => void; + unregisterFromEvents: (viewTag: number) => void; +} + export interface AnimatedStyleHandle< Style extends DefaultStyle = DefaultStyle > { diff --git a/src/reanimated2/hook/useEvent.ts b/src/reanimated2/hook/useEvent.ts index f70e7eaa82bb..dbd750d1f5d7 100644 --- a/src/reanimated2/hook/useEvent.ts +++ b/src/reanimated2/hook/useEvent.ts @@ -1,7 +1,7 @@ 'use strict'; import { useRef } from 'react'; -import WorkletEventHandler from '../WorkletEventHandler'; -import type { ReanimatedEvent } from './commonTypes'; +import { WorkletEventHandler } from '../WorkletEventHandler'; +import type { IWorkletEventHandler, ReanimatedEvent } from './commonTypes'; /** * Worklet to provide as an argument to `useEvent` hook. @@ -17,7 +17,7 @@ export type EventHandlerProcessed< > = (event: Event, context?: Context) => void; export type EventHandlerInternal = { - workletEventHandler: WorkletEventHandler; + workletEventHandler: IWorkletEventHandler; }; /** @@ -57,7 +57,7 @@ export function useEvent( initRef.current = { workletEventHandler }; } else if (rebuild) { const workletEventHandler = initRef.current.workletEventHandler; - workletEventHandler.updateWorklet(handler); + workletEventHandler.updateEventHandler(handler, eventNames); initRef.current = { workletEventHandler }; } diff --git a/src/reanimated2/hook/useScrollViewOffset.ts b/src/reanimated2/hook/useScrollViewOffset.ts index b2981f26e017..12e19401392f 100644 --- a/src/reanimated2/hook/useScrollViewOffset.ts +++ b/src/reanimated2/hook/useScrollViewOffset.ts @@ -73,6 +73,7 @@ function useScrollViewOffsetNative( const internalOffset = useSharedValue(0); const offset = useRef(providedOffset ?? internalOffset).current; const scrollRef = useRef(null); + const scrollRefTag = useRef(null); const eventHandler = useEvent( (event: ReanimatedScrollEvent) => { @@ -89,15 +90,30 @@ function useScrollViewOffsetNative( useEffect(() => { // We need to make sure that listener for old animatedRef value is removed - if (scrollRef.current !== null) { - eventHandler.workletEventHandler.unregisterFromEvents(); + if (scrollRef.current !== null && scrollRefTag.current !== null) { + eventHandler.workletEventHandler.unregisterFromEvents( + scrollRefTag.current + ); } + + // Store the ref and viewTag for future cleanup scrollRef.current = animatedRef.current; + scrollRefTag.current = animatedRef.getTag(); + + if (scrollRefTag === null) { + console.warn( + '[Reanimated] ScrollViewOffset failed to resolve the view tag from animated ref. Did you forget to attach the ref to a component?' + ); + } else { + eventHandler.workletEventHandler.registerForEvents(scrollRefTag.current); + } - const viewTag = animatedRef.getTag(); - eventHandler.workletEventHandler.registerForEvents(viewTag); return () => { - eventHandler.workletEventHandler.unregisterFromEvents(); + if (scrollRefTag.current !== null) { + eventHandler.workletEventHandler.unregisterFromEvents( + scrollRefTag.current + ); + } }; // React here has a problem with `animatedRef.current` since a Ref .current // field shouldn't be used as a dependency. However, in this case we have