From 87c27e09139079aaccf92178073874ba38f359fe Mon Sep 17 00:00:00 2001 From: szydlovsky <9szydlowski9@gmail.com> Date: Wed, 27 Mar 2024 18:06:47 +0100 Subject: [PATCH 01/14] Revamped WorkletEventHandler --- src/createAnimatedComponent/PropsFilter.tsx | 2 +- .../createAnimatedComponent.tsx | 79 ++++----- src/reanimated2/WorkletEventHandler.ts | 153 +++++++++++++----- src/reanimated2/hook/commonTypes.ts | 9 ++ src/reanimated2/hook/useEvent.ts | 8 +- src/reanimated2/hook/useScrollViewOffset.ts | 8 +- 6 files changed, 172 insertions(+), 87 deletions(-) diff --git a/src/createAnimatedComponent/PropsFilter.tsx b/src/createAnimatedComponent/PropsFilter.tsx index 78932cfd508..15671bae20d 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'; import { isSharedValue } from '../reanimated2'; 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 7a9e067a65c..1027f2ab71a 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'; @@ -55,6 +55,7 @@ import { updateLayoutAnimations } from '../reanimated2/UpdateLayoutAnimations'; import type { CustomConfig } from '../reanimated2/layoutReanimation/web/config'; import type { FlatList, FlatListProps } from 'react-native'; import { addHTMLMutationObserver } from '../reanimated2/layoutReanimation/web/domUtils'; +import type { IWorkletEventHandler } from 'src/reanimated2/hook/commonTypes'; const IS_WEB = isWeb(); @@ -253,61 +254,63 @@ export function createAnimatedComponent( has('workletEventHandler', prop) && prop.workletEventHandler instanceof WorkletEventHandler ) { - prop.workletEventHandler.unregisterFromEvents(); - } - } - } - - _detachStyles() { - if (IS_WEB && this._styles !== null) { - for (const style of this._styles) { - style.viewsRef.remove(this); - } - } else if (this._viewTag !== -1 && this._styles !== null) { - for (const style of this._styles) { - style.viewDescriptors.remove(this._viewTag); - } - if (this.props.animatedProps?.viewDescriptors) { - this.props.animatedProps.viewDescriptors.remove(this._viewTag); - } - if (isFabric()) { - removeFromPropsRegistry(this._viewTag); + prop.workletEventHandler.unregisterFromEvents( + this._getViewInfo().viewTag as number + ); } } } - _reattachNativeEvents( + _updateNativeEvents( prevProps: AnimatedComponentProps ) { + let previousHandler: IWorkletEventHandler | undefined; + let newHandler: IWorkletEventHandler | undefined; + + // Get previous handler for (const key in prevProps) { - const prop = this.props[key]; + const prop = prevProps[key]; if ( has('workletEventHandler', prop) && - prop.workletEventHandler instanceof WorkletEventHandler && - prop.workletEventHandler.reattachNeeded + prop.workletEventHandler instanceof WorkletEventHandler ) { - prop.workletEventHandler.unregisterFromEvents(); + previousHandler = prop.workletEventHandler; } } - let viewTag = null; - + // Get new handler for (const key in this.props) { const prop = this.props[key]; if ( has('workletEventHandler', prop) && - prop.workletEventHandler instanceof WorkletEventHandler && - prop.workletEventHandler.reattachNeeded + prop.workletEventHandler instanceof WorkletEventHandler ) { - if (viewTag === null) { - const node = this._getEventViewRef() as AnimatedComponentRef; + newHandler = prop.workletEventHandler; + } + } - viewTag = IS_WEB - ? this._component - : findNodeHandle(options?.setNativeProps ? this : node); - } - prop.workletEventHandler.registerForEvents(viewTag as number, key); - prop.workletEventHandler.reattachNeeded = false; + if (previousHandler !== newHandler) { + // Handler changed, we need to unregister from the previous one and register to the new one + const viewTag = this._getViewInfo().viewTag as number; + previousHandler?.unregisterFromEvents(viewTag); + newHandler?.registerForEvents(viewTag); + } + } + + _detachStyles() { + if (IS_WEB && this._styles !== null) { + for (const style of this._styles) { + style.viewsRef.remove(this); + } + } else if (this._viewTag !== -1 && this._styles !== null) { + for (const style of this._styles) { + style.viewDescriptors.remove(this._viewTag); + } + if (this.props.animatedProps?.viewDescriptors) { + this.props.animatedProps.viewDescriptors.remove(this._viewTag); + } + if (isFabric()) { + removeFromPropsRegistry(this._viewTag); } } } @@ -464,7 +467,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 6b233132a5c..f9bab9cb4bd 100644 --- a/src/reanimated2/WorkletEventHandler.ts +++ b/src/reanimated2/WorkletEventHandler.ts @@ -1,13 +1,93 @@ '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(); -type JSEvent = NativeSyntheticEvent>; +type EventHandlerRegistration = { + id: number; + viewTag: number; +}; + +class WorkletEventHandlerNative + implements IWorkletEventHandler +{ + worklet: (event: ReanimatedEvent) => void; + eventNames: string[]; + viewTags: Set; + registrations: EventHandlerRegistration[]; + constructor( + worklet: (event: ReanimatedEvent) => void, + eventNames: string[] = [] + ) { + this.worklet = worklet; + this.eventNames = eventNames; + this.viewTags = new Set(); + this.registrations = []; + } + + 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((registration) => { + unregisterEventHandler(registration.id); + }); + this.registrations = []; + + // Attach new events with new worklet + this.registrations = this.eventNames.flatMap((eventName) => { + return Array.from(this.viewTags).map((tag) => { + return { + id: registerEventHandler(this.worklet, eventName, tag), + viewTag: tag, + }; + }); + }); + } + + registerForEvents(viewTag: number, fallbackEventName?: string): void { + this.viewTags.add(viewTag); + this.registrations = this.registrations.concat( + this.eventNames.map((eventName) => { + return { + id: registerEventHandler(this.worklet, eventName, viewTag), + viewTag, + }; + }) + ); + + if (this.eventNames.length === 0 && fallbackEventName) { + this.registrations.push({ + id: registerEventHandler(this.worklet, fallbackEventName, viewTag), + viewTag, + }); + } + } + + unregisterFromEvents(viewTag: number): void { + this.viewTags.delete(viewTag); + this.registrations.forEach((registration) => { + if (registration.viewTag === viewTag) { + unregisterEventHandler(registration.id); + } + }); + } +} + +type JSEvent = NativeSyntheticEvent>; // In JS implementation (e.g. for web) we don't use Reanimated's // event emitter, therefore we have to handle here // the event that came from React Native and convert it. @@ -20,64 +100,57 @@ function jsListener( }; } -export default class WorkletEventHandler { +class WorkletEventHandlerWeb + implements IWorkletEventHandler +{ worklet: (event: ReanimatedEvent) => void; eventNames: string[]; - reattachNeeded: boolean; listeners: | Record>) => void> | Record) => void>; - viewTag: number | undefined; - registrations: number[]; 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.reduce( + ( + acc: Record) => void>, + eventName: string + ) => { + acc[eventName] = jsListener(eventName, this.worklet); + return acc; + }, + {} + ); } - 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 e294c9b10db..f65217f1070 100644 --- a/src/reanimated2/hook/commonTypes.ts +++ b/src/reanimated2/hook/commonTypes.ts @@ -78,6 +78,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 f70e7eaa82b..dbd750d1f5d 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 70433cf026a..01b251100d7 100644 --- a/src/reanimated2/hook/useScrollViewOffset.ts +++ b/src/reanimated2/hook/useScrollViewOffset.ts @@ -99,15 +99,15 @@ function useScrollViewOffsetNative( useEffect(() => { // We need to make sure that listener for old animatedRef value is removed if (scrollRef.current !== null) { - eventHandler.workletEventHandler.unregisterFromEvents(); + const oldTag = findNodeHandle(scrollRef.current); + eventHandler.workletEventHandler.unregisterFromEvents(oldTag as number); } scrollRef.current = animatedRef.current; - const component = animatedRef.current; - const viewTag = findNodeHandle(component); + const viewTag = findNodeHandle(animatedRef.current); eventHandler.workletEventHandler.registerForEvents(viewTag as number); return () => { - eventHandler.workletEventHandler.unregisterFromEvents(); + eventHandler.workletEventHandler.unregisterFromEvents(viewTag as number); }; // 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 From 86e762b139bb40d0beeecc0e2c949b744536103b Mon Sep 17 00:00:00 2001 From: szydlovsky <9szydlowski9@gmail.com> Date: Wed, 27 Mar 2024 18:39:51 +0100 Subject: [PATCH 02/14] fix import --- src/createAnimatedComponent/createAnimatedComponent.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/createAnimatedComponent/createAnimatedComponent.tsx b/src/createAnimatedComponent/createAnimatedComponent.tsx index e004584e823..3c2aceae56e 100644 --- a/src/createAnimatedComponent/createAnimatedComponent.tsx +++ b/src/createAnimatedComponent/createAnimatedComponent.tsx @@ -55,7 +55,7 @@ import { updateLayoutAnimations } from '../reanimated2/UpdateLayoutAnimations'; import type { CustomConfig } from '../reanimated2/layoutReanimation/web/config'; import type { FlatList, FlatListProps } from 'react-native'; import { addHTMLMutationObserver } from '../reanimated2/layoutReanimation/web/domUtils'; -import type { IWorkletEventHandler } from 'src/reanimated2/hook/commonTypes'; +import type { IWorkletEventHandler } from '../reanimated2/hook/commonTypes'; import { getViewInfo } from './getViewInfo'; const IS_WEB = isWeb(); From 42adc7453ff95b9496ca3522f88c30a135e28235 Mon Sep 17 00:00:00 2001 From: szydlovsky <9szydlowski9@gmail.com> Date: Thu, 28 Mar 2024 18:00:49 +0100 Subject: [PATCH 03/14] code review changes and fixes --- .../createAnimatedComponent.tsx | 22 +--- src/reanimated2/WorkletEventHandler.ts | 112 ++++++++---------- src/reanimated2/hook/useScrollViewOffset.ts | 18 ++- 3 files changed, 70 insertions(+), 82 deletions(-) diff --git a/src/createAnimatedComponent/createAnimatedComponent.tsx b/src/createAnimatedComponent/createAnimatedComponent.tsx index 3c2aceae56e..a11f5c0a2a8 100644 --- a/src/createAnimatedComponent/createAnimatedComponent.tsx +++ b/src/createAnimatedComponent/createAnimatedComponent.tsx @@ -255,9 +255,7 @@ export function createAnimatedComponent( has('workletEventHandler', prop) && prop.workletEventHandler instanceof WorkletEventHandler ) { - prop.workletEventHandler.unregisterFromEvents( - this._getViewInfo().viewTag as number - ); + prop.workletEventHandler.unregisterFromEvents(this._viewTag); } } } @@ -265,37 +263,27 @@ export function createAnimatedComponent( _updateNativeEvents( prevProps: AnimatedComponentProps ) { - let previousHandler: IWorkletEventHandler | undefined; - let newHandler: IWorkletEventHandler | undefined; - - // Get previous handler + // Unregister from previous handlers for (const key in prevProps) { const prop = prevProps[key]; if ( has('workletEventHandler', prop) && prop.workletEventHandler instanceof WorkletEventHandler ) { - previousHandler = prop.workletEventHandler; + prop.workletEventHandler.unregisterFromEvents(this._viewTag); } } - // Get new handler + // Register for new handlers for (const key in this.props) { const prop = this.props[key]; if ( has('workletEventHandler', prop) && prop.workletEventHandler instanceof WorkletEventHandler ) { - newHandler = prop.workletEventHandler; + prop.workletEventHandler.registerForEvents(this._viewTag); } } - - if (previousHandler !== newHandler) { - // Handler changed, we need to unregister from the previous one and register to the new one - const viewTag = this._getViewInfo().viewTag as number; - previousHandler?.unregisterFromEvents(viewTag); - newHandler?.registerForEvents(viewTag); - } } _detachStyles() { diff --git a/src/reanimated2/WorkletEventHandler.ts b/src/reanimated2/WorkletEventHandler.ts index f9bab9cb4bd..cba7d3d8329 100644 --- a/src/reanimated2/WorkletEventHandler.ts +++ b/src/reanimated2/WorkletEventHandler.ts @@ -10,26 +10,21 @@ import { shouldBeUseWeb } from './PlatformChecker'; const SHOULD_BE_USE_WEB = shouldBeUseWeb(); -type EventHandlerRegistration = { - id: number; - viewTag: number; -}; - class WorkletEventHandlerNative implements IWorkletEventHandler { - worklet: (event: ReanimatedEvent) => void; eventNames: string[]; - viewTags: Set; - registrations: EventHandlerRegistration[]; + #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.#worklet = worklet; this.eventNames = eventNames; - this.viewTags = new Set(); - this.registrations = []; + this.#viewTags = new Set(); + this.#registrations = new Map(); } updateEventHandler( @@ -37,75 +32,59 @@ class WorkletEventHandlerNative newEvents: string[] ): void { // Update worklet and event names - this.worklet = newWorklet; + this.#worklet = newWorklet; this.eventNames = newEvents; // Detach all events - this.registrations.forEach((registration) => { - unregisterEventHandler(registration.id); + this.#registrations.forEach((registrationIDs, tag) => { + registrationIDs.forEach((id) => { + unregisterEventHandler(id); + }); + this.#registrations.set(tag, []); }); - this.registrations = []; // Attach new events with new worklet - this.registrations = this.eventNames.flatMap((eventName) => { - return Array.from(this.viewTags).map((tag) => { - return { - id: registerEventHandler(this.worklet, eventName, tag), - viewTag: tag, - }; + Array.from(this.#viewTags).forEach((tag) => { + const newRegistrations = this.eventNames.map((eventName) => { + return registerEventHandler(this.#worklet, eventName, tag); }); + this.#registrations.set(tag, newRegistrations); }); } registerForEvents(viewTag: number, fallbackEventName?: string): void { - this.viewTags.add(viewTag); - - this.registrations = this.registrations.concat( - this.eventNames.map((eventName) => { - return { - id: registerEventHandler(this.worklet, eventName, viewTag), - viewTag, - }; - }) - ); + this.#viewTags.add(viewTag); + + const newRegistrations = this.eventNames.map((eventName) => { + return registerEventHandler(this.#worklet, eventName, viewTag); + }); + this.#registrations.set(viewTag, newRegistrations); if (this.eventNames.length === 0 && fallbackEventName) { - this.registrations.push({ - id: registerEventHandler(this.worklet, fallbackEventName, viewTag), - viewTag, - }); + const newRegistration = registerEventHandler( + this.#worklet, + fallbackEventName, + viewTag + ); + this.#registrations.set(viewTag, [newRegistration]); } } unregisterFromEvents(viewTag: number): void { - this.viewTags.delete(viewTag); - this.registrations.forEach((registration) => { - if (registration.viewTag === viewTag) { - unregisterEventHandler(registration.id); - } + this.#viewTags.delete(viewTag); + this.#registrations.get(viewTag)?.forEach((id) => { + unregisterEventHandler(id); }); + this.#registrations.delete(viewTag); } } -type JSEvent = NativeSyntheticEvent>; -// In JS implementation (e.g. for web) we don't use Reanimated's -// event emitter, therefore we have to handle here -// the event that came from React Native and convert it. -function jsListener( - eventName: string, - handler: (event: ReanimatedEvent) => void -) { - return (evt: JSEvent) => { - handler({ ...evt.nativeEvent, eventName } as ReanimatedEvent); - }; -} - class WorkletEventHandlerWeb implements IWorkletEventHandler { - worklet: (event: ReanimatedEvent) => void; eventNames: string[]; - listeners: + #worklet: (event: ReanimatedEvent) => void; + #listeners: | Record>) => void> | Record) => void>; @@ -113,19 +92,19 @@ class WorkletEventHandlerWeb worklet: (event: ReanimatedEvent) => void, eventNames: string[] = [] ) { - this.worklet = worklet; + this.#worklet = worklet; this.eventNames = eventNames; - this.listeners = {}; + this.#listeners = {}; this.setupWebListeners(); } setupWebListeners() { - this.listeners = this.eventNames.reduce( + this.#listeners = this.eventNames.reduce( ( acc: Record) => void>, eventName: string ) => { - acc[eventName] = jsListener(eventName, this.worklet); + acc[eventName] = jsListener(eventName, this.#worklet); return acc; }, {} @@ -137,7 +116,7 @@ class WorkletEventHandlerWeb newEvents: string[] ): void { // Update worklet and event names - this.worklet = newWorklet; + this.#worklet = newWorklet; this.eventNames = newEvents; this.setupWebListeners(); } @@ -151,6 +130,19 @@ class WorkletEventHandlerWeb } } +type JSEvent = NativeSyntheticEvent>; +// In JS implementation (e.g. for web) we don't use Reanimated's +// event emitter, therefore we have to handle here +// the event that came from React Native and convert it. +function jsListener( + eventName: string, + handler: (event: ReanimatedEvent) => void +) { + return (evt: JSEvent) => { + handler({ ...evt.nativeEvent, eventName } as ReanimatedEvent); + }; +} + export const WorkletEventHandler = SHOULD_BE_USE_WEB ? WorkletEventHandlerWeb : WorkletEventHandlerNative; diff --git a/src/reanimated2/hook/useScrollViewOffset.ts b/src/reanimated2/hook/useScrollViewOffset.ts index 01b251100d7..5c066e8992c 100644 --- a/src/reanimated2/hook/useScrollViewOffset.ts +++ b/src/reanimated2/hook/useScrollViewOffset.ts @@ -82,6 +82,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) => { @@ -99,15 +100,22 @@ function useScrollViewOffsetNative( useEffect(() => { // We need to make sure that listener for old animatedRef value is removed if (scrollRef.current !== null) { - const oldTag = findNodeHandle(scrollRef.current); - eventHandler.workletEventHandler.unregisterFromEvents(oldTag as number); + eventHandler.workletEventHandler.unregisterFromEvents( + scrollRefTag.current as number + ); } + + // Store the ref and viewTag for future cleanup scrollRef.current = animatedRef.current; + scrollRefTag.current = findNodeHandle(scrollRef.current); - const viewTag = findNodeHandle(animatedRef.current); - eventHandler.workletEventHandler.registerForEvents(viewTag as number); + eventHandler.workletEventHandler.registerForEvents( + scrollRefTag.current as number + ); return () => { - eventHandler.workletEventHandler.unregisterFromEvents(viewTag as number); + eventHandler.workletEventHandler.unregisterFromEvents( + scrollRefTag.current as number + ); }; // 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 From 30caad9802abdc885bce8518e992b55498f5ba98 Mon Sep 17 00:00:00 2001 From: szydlovsky <9szydlowski9@gmail.com> Date: Thu, 28 Mar 2024 18:05:57 +0100 Subject: [PATCH 04/14] fix lint again --- src/createAnimatedComponent/createAnimatedComponent.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/createAnimatedComponent/createAnimatedComponent.tsx b/src/createAnimatedComponent/createAnimatedComponent.tsx index a11f5c0a2a8..9aabcb0191e 100644 --- a/src/createAnimatedComponent/createAnimatedComponent.tsx +++ b/src/createAnimatedComponent/createAnimatedComponent.tsx @@ -55,7 +55,6 @@ import { updateLayoutAnimations } from '../reanimated2/UpdateLayoutAnimations'; import type { CustomConfig } from '../reanimated2/layoutReanimation/web/config'; import type { FlatList, FlatListProps } from 'react-native'; import { addHTMLMutationObserver } from '../reanimated2/layoutReanimation/web/domUtils'; -import type { IWorkletEventHandler } from '../reanimated2/hook/commonTypes'; import { getViewInfo } from './getViewInfo'; const IS_WEB = isWeb(); From 614c68877596b1c228292fc84c58c778bc93eca4 Mon Sep 17 00:00:00 2001 From: szydlovsky <9szydlowski9@gmail.com> Date: Thu, 28 Mar 2024 18:25:34 +0100 Subject: [PATCH 05/14] fix publicity of handler props --- src/reanimated2/WorkletEventHandler.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/reanimated2/WorkletEventHandler.ts b/src/reanimated2/WorkletEventHandler.ts index cba7d3d8329..a7d5ef121a0 100644 --- a/src/reanimated2/WorkletEventHandler.ts +++ b/src/reanimated2/WorkletEventHandler.ts @@ -83,10 +83,11 @@ class WorkletEventHandlerWeb implements IWorkletEventHandler { eventNames: string[]; - #worklet: (event: ReanimatedEvent) => void; - #listeners: + listeners: | Record>) => void> | Record) => void>; + + #worklet: (event: ReanimatedEvent) => void; constructor( worklet: (event: ReanimatedEvent) => void, @@ -94,12 +95,12 @@ class WorkletEventHandlerWeb ) { this.#worklet = worklet; this.eventNames = eventNames; - this.#listeners = {}; + this.listeners = {}; this.setupWebListeners(); } setupWebListeners() { - this.#listeners = this.eventNames.reduce( + this.listeners = this.eventNames.reduce( ( acc: Record) => void>, eventName: string From 88583b272e32cfec04918930694ab6e0adca8c5a Mon Sep 17 00:00:00 2001 From: szydlovsky <9szydlowski9@gmail.com> Date: Thu, 28 Mar 2024 18:29:12 +0100 Subject: [PATCH 06/14] linter give me a break --- src/reanimated2/WorkletEventHandler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/reanimated2/WorkletEventHandler.ts b/src/reanimated2/WorkletEventHandler.ts index a7d5ef121a0..696471b08c7 100644 --- a/src/reanimated2/WorkletEventHandler.ts +++ b/src/reanimated2/WorkletEventHandler.ts @@ -86,7 +86,7 @@ class WorkletEventHandlerWeb listeners: | Record>) => void> | Record) => void>; - + #worklet: (event: ReanimatedEvent) => void; constructor( From 20b8d67939310f2e897fd85d091042434035943e Mon Sep 17 00:00:00 2001 From: szydlovsky <9szydlowski9@gmail.com> Date: Fri, 29 Mar 2024 15:57:38 +0100 Subject: [PATCH 07/14] improved scroll handler props changes --- .../createAnimatedComponent.tsx | 32 +++++++++++++------ 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/src/createAnimatedComponent/createAnimatedComponent.tsx b/src/createAnimatedComponent/createAnimatedComponent.tsx index 9aabcb0191e..c1720f00f4e 100644 --- a/src/createAnimatedComponent/createAnimatedComponent.tsx +++ b/src/createAnimatedComponent/createAnimatedComponent.tsx @@ -262,25 +262,37 @@ export function createAnimatedComponent( _updateNativeEvents( prevProps: AnimatedComponentProps ) { - // Unregister from previous handlers for (const key in prevProps) { - const prop = prevProps[key]; + const prevProp = prevProps[key]; if ( - has('workletEventHandler', prop) && - prop.workletEventHandler instanceof WorkletEventHandler + has('workletEventHandler', prevProp) && + prevProp.workletEventHandler instanceof WorkletEventHandler ) { - prop.workletEventHandler.unregisterFromEvents(this._viewTag); + const newProp = this.props[key]; + if (newProp === null) { + // 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); + } } } - // Register for new handlers for (const key in this.props) { - const prop = this.props[key]; + const newProp = this.props[key]; if ( - has('workletEventHandler', prop) && - prop.workletEventHandler instanceof WorkletEventHandler + has('workletEventHandler', newProp) && + newProp.workletEventHandler instanceof WorkletEventHandler && + prevProps[key] === null ) { - prop.workletEventHandler.registerForEvents(this._viewTag); + // Prop got added + newProp.workletEventHandler.registerForEvents(this._viewTag); } } } From b77737d3689ef44205148668fde0fa58aaebef65 Mon Sep 17 00:00:00 2001 From: szydlovsky <9szydlowski9@gmail.com> Date: Fri, 29 Mar 2024 16:07:52 +0100 Subject: [PATCH 08/14] fix null/undefined check --- src/createAnimatedComponent/createAnimatedComponent.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/createAnimatedComponent/createAnimatedComponent.tsx b/src/createAnimatedComponent/createAnimatedComponent.tsx index c1720f00f4e..7deb4fe5d94 100644 --- a/src/createAnimatedComponent/createAnimatedComponent.tsx +++ b/src/createAnimatedComponent/createAnimatedComponent.tsx @@ -269,7 +269,7 @@ export function createAnimatedComponent( prevProp.workletEventHandler instanceof WorkletEventHandler ) { const newProp = this.props[key]; - if (newProp === null) { + if (!newProp) { // Prop got deleted prevProp.workletEventHandler.unregisterFromEvents(this._viewTag); } else if ( @@ -289,7 +289,7 @@ export function createAnimatedComponent( if ( has('workletEventHandler', newProp) && newProp.workletEventHandler instanceof WorkletEventHandler && - prevProps[key] === null + !prevProps[key] ) { // Prop got added newProp.workletEventHandler.registerForEvents(this._viewTag); From 9ba75de8a9d0d6b1c1d8879d2e4f66e5b17d99fe Mon Sep 17 00:00:00 2001 From: szydlovsky <9szydlowski9@gmail.com> Date: Tue, 2 Apr 2024 15:53:19 +0200 Subject: [PATCH 09/14] further code review improvements --- .../createAnimatedComponent.tsx | 36 +++++++-------- src/reanimated2/WorkletEventHandler.ts | 44 ++++++++----------- 2 files changed, 37 insertions(+), 43 deletions(-) diff --git a/src/createAnimatedComponent/createAnimatedComponent.tsx b/src/createAnimatedComponent/createAnimatedComponent.tsx index 7deb4fe5d94..f9f9f38b875 100644 --- a/src/createAnimatedComponent/createAnimatedComponent.tsx +++ b/src/createAnimatedComponent/createAnimatedComponent.tsx @@ -259,6 +259,24 @@ export function createAnimatedComponent( } } + _detachStyles() { + if (IS_WEB && this._styles !== null) { + for (const style of this._styles) { + style.viewsRef.remove(this); + } + } else if (this._viewTag !== -1 && this._styles !== null) { + for (const style of this._styles) { + style.viewDescriptors.remove(this._viewTag); + } + if (this.props.animatedProps?.viewDescriptors) { + this.props.animatedProps.viewDescriptors.remove(this._viewTag); + } + if (isFabric()) { + removeFromPropsRegistry(this._viewTag); + } + } + } + _updateNativeEvents( prevProps: AnimatedComponentProps ) { @@ -297,24 +315,6 @@ export function createAnimatedComponent( } } - _detachStyles() { - if (IS_WEB && this._styles !== null) { - for (const style of this._styles) { - style.viewsRef.remove(this); - } - } else if (this._viewTag !== -1 && this._styles !== null) { - for (const style of this._styles) { - style.viewDescriptors.remove(this._viewTag); - } - if (this.props.animatedProps?.viewDescriptors) { - this.props.animatedProps.viewDescriptors.remove(this._viewTag); - } - if (isFabric()) { - removeFromPropsRegistry(this._viewTag); - } - } - } - _updateFromNative(props: StyleProps) { if (options?.setNativeProps) { options.setNativeProps(this._component as AnimatedComponentRef, props); diff --git a/src/reanimated2/WorkletEventHandler.ts b/src/reanimated2/WorkletEventHandler.ts index 696471b08c7..7e3cadcaf3c 100644 --- a/src/reanimated2/WorkletEventHandler.ts +++ b/src/reanimated2/WorkletEventHandler.ts @@ -10,6 +10,19 @@ import { shouldBeUseWeb } from './PlatformChecker'; const SHOULD_BE_USE_WEB = shouldBeUseWeb(); +type JSEvent = NativeSyntheticEvent>; +// In JS implementation (e.g. for web) we don't use Reanimated's +// event emitter, therefore we have to handle here +// the event that came from React Native and convert it. +function jsListener( + eventName: string, + handler: (event: ReanimatedEvent) => void +) { + return (evt: JSEvent) => { + handler({ ...evt.nativeEvent, eventName } as ReanimatedEvent); + }; +} + class WorkletEventHandlerNative implements IWorkletEventHandler { @@ -36,11 +49,11 @@ class WorkletEventHandlerNative this.eventNames = newEvents; // Detach all events - this.#registrations.forEach((registrationIDs, tag) => { + this.#registrations.forEach((registrationIDs, _tag) => { registrationIDs.forEach((id) => { unregisterEventHandler(id); }); - this.#registrations.set(tag, []); + // No need to remove registrationIDs from map, since it gets overwritten when attaching }); // Attach new events with new worklet @@ -100,16 +113,10 @@ class WorkletEventHandlerWeb } setupWebListeners() { - this.listeners = this.eventNames.reduce( - ( - acc: Record) => void>, - eventName: string - ) => { - acc[eventName] = jsListener(eventName, this.#worklet); - return acc; - }, - {} - ); + this.listeners = {}; + this.eventNames.forEach((eventName) => { + this.listeners[eventName] = jsListener(eventName, this.#worklet); + }); } updateEventHandler( @@ -131,19 +138,6 @@ class WorkletEventHandlerWeb } } -type JSEvent = NativeSyntheticEvent>; -// In JS implementation (e.g. for web) we don't use Reanimated's -// event emitter, therefore we have to handle here -// the event that came from React Native and convert it. -function jsListener( - eventName: string, - handler: (event: ReanimatedEvent) => void -) { - return (evt: JSEvent) => { - handler({ ...evt.nativeEvent, eventName } as ReanimatedEvent); - }; -} - export const WorkletEventHandler = SHOULD_BE_USE_WEB ? WorkletEventHandlerWeb : WorkletEventHandlerNative; From 7de562acfa9e91d13facddf6acec36d9ce966ecb Mon Sep 17 00:00:00 2001 From: szydlovsky <9szydlowski9@gmail.com> Date: Wed, 3 Apr 2024 10:54:55 +0200 Subject: [PATCH 10/14] make worklet a public API for now --- src/reanimated2/WorkletEventHandler.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/reanimated2/WorkletEventHandler.ts b/src/reanimated2/WorkletEventHandler.ts index 7e3cadcaf3c..baa648c1974 100644 --- a/src/reanimated2/WorkletEventHandler.ts +++ b/src/reanimated2/WorkletEventHandler.ts @@ -27,14 +27,14 @@ class WorkletEventHandlerNative implements IWorkletEventHandler { eventNames: string[]; - #worklet: (event: ReanimatedEvent) => void; + 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.worklet = worklet; this.eventNames = eventNames; this.#viewTags = new Set(); this.#registrations = new Map(); @@ -45,7 +45,7 @@ class WorkletEventHandlerNative newEvents: string[] ): void { // Update worklet and event names - this.#worklet = newWorklet; + this.worklet = newWorklet; this.eventNames = newEvents; // Detach all events @@ -59,7 +59,7 @@ class WorkletEventHandlerNative // Attach new events with new worklet Array.from(this.#viewTags).forEach((tag) => { const newRegistrations = this.eventNames.map((eventName) => { - return registerEventHandler(this.#worklet, eventName, tag); + return registerEventHandler(this.worklet, eventName, tag); }); this.#registrations.set(tag, newRegistrations); }); @@ -69,13 +69,13 @@ class WorkletEventHandlerNative this.#viewTags.add(viewTag); const newRegistrations = this.eventNames.map((eventName) => { - return registerEventHandler(this.#worklet, eventName, viewTag); + return registerEventHandler(this.worklet, eventName, viewTag); }); this.#registrations.set(viewTag, newRegistrations); if (this.eventNames.length === 0 && fallbackEventName) { const newRegistration = registerEventHandler( - this.#worklet, + this.worklet, fallbackEventName, viewTag ); @@ -100,13 +100,13 @@ class WorkletEventHandlerWeb | Record>) => void> | Record) => void>; - #worklet: (event: ReanimatedEvent) => void; + worklet: (event: ReanimatedEvent) => void; constructor( worklet: (event: ReanimatedEvent) => void, eventNames: string[] = [] ) { - this.#worklet = worklet; + this.worklet = worklet; this.eventNames = eventNames; this.listeners = {}; this.setupWebListeners(); @@ -115,7 +115,7 @@ class WorkletEventHandlerWeb setupWebListeners() { this.listeners = {}; this.eventNames.forEach((eventName) => { - this.listeners[eventName] = jsListener(eventName, this.#worklet); + this.listeners[eventName] = jsListener(eventName, this.worklet); }); } @@ -124,7 +124,7 @@ class WorkletEventHandlerWeb newEvents: string[] ): void { // Update worklet and event names - this.#worklet = newWorklet; + this.worklet = newWorklet; this.eventNames = newEvents; this.setupWebListeners(); } From a2de0b79ab94b8dd21e0f388104eb63cdd1ee7a9 Mon Sep 17 00:00:00 2001 From: szydlovsky <9szydlowski9@gmail.com> Date: Wed, 3 Apr 2024 17:52:26 +0200 Subject: [PATCH 11/14] small post main merge changes --- src/reanimated2/hook/useScrollViewOffset.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/reanimated2/hook/useScrollViewOffset.ts b/src/reanimated2/hook/useScrollViewOffset.ts index 3dececcba57..9b87a795640 100644 --- a/src/reanimated2/hook/useScrollViewOffset.ts +++ b/src/reanimated2/hook/useScrollViewOffset.ts @@ -90,9 +90,9 @@ function useScrollViewOffsetNative( useEffect(() => { // We need to make sure that listener for old animatedRef value is removed - if (scrollRef.current !== null) { + if (scrollRef.current !== null && scrollRefTag.current !== null) { eventHandler.workletEventHandler.unregisterFromEvents( - scrollRefTag.current as number + scrollRefTag.current ); } @@ -100,13 +100,13 @@ function useScrollViewOffsetNative( scrollRef.current = animatedRef.current; scrollRefTag.current = animatedRef.getTag(); - eventHandler.workletEventHandler.registerForEvents( - scrollRefTag.current as number - ); + eventHandler.workletEventHandler.registerForEvents(scrollRefTag.current); return () => { - eventHandler.workletEventHandler.unregisterFromEvents( - scrollRefTag.current as number - ); + 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 From ad1fe896f020a1058f23ea50a45d87501af1004b Mon Sep 17 00:00:00 2001 From: szydlovsky <9szydlowski9@gmail.com> Date: Thu, 4 Apr 2024 11:20:08 +0200 Subject: [PATCH 12/14] add null guard and a warn to scroll tag logic --- src/reanimated2/hook/useScrollViewOffset.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/reanimated2/hook/useScrollViewOffset.ts b/src/reanimated2/hook/useScrollViewOffset.ts index 9b87a795640..12e19401392 100644 --- a/src/reanimated2/hook/useScrollViewOffset.ts +++ b/src/reanimated2/hook/useScrollViewOffset.ts @@ -100,7 +100,14 @@ function useScrollViewOffsetNative( scrollRef.current = animatedRef.current; scrollRefTag.current = animatedRef.getTag(); - eventHandler.workletEventHandler.registerForEvents(scrollRefTag.current); + 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); + } + return () => { if (scrollRefTag.current !== null) { eventHandler.workletEventHandler.unregisterFromEvents( From 4e911b82e56480453036c50b8928c96efd31b0b7 Mon Sep 17 00:00:00 2001 From: szydlovsky <9szydlowski9@gmail.com> Date: Fri, 5 Apr 2024 11:26:51 +0200 Subject: [PATCH 13/14] cosmetical changes in worklet handler --- src/reanimated2/WorkletEventHandler.ts | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/reanimated2/WorkletEventHandler.ts b/src/reanimated2/WorkletEventHandler.ts index baa648c1974..439efaad769 100644 --- a/src/reanimated2/WorkletEventHandler.ts +++ b/src/reanimated2/WorkletEventHandler.ts @@ -11,6 +11,7 @@ import { shouldBeUseWeb } from './PlatformChecker'; const SHOULD_BE_USE_WEB = shouldBeUseWeb(); type JSEvent = NativeSyntheticEvent>; + // In JS implementation (e.g. for web) we don't use Reanimated's // event emitter, therefore we have to handle here // the event that came from React Native and convert it. @@ -32,7 +33,7 @@ class WorkletEventHandlerNative #registrations: Map; // keys are viewTags, values are arrays of registration ID's for each viewTag constructor( worklet: (event: ReanimatedEvent) => void, - eventNames: string[] = [] + eventNames: string[] ) { this.worklet = worklet; this.eventNames = eventNames; @@ -49,18 +50,16 @@ class WorkletEventHandlerNative this.eventNames = newEvents; // Detach all events - this.#registrations.forEach((registrationIDs, _tag) => { - registrationIDs.forEach((id) => { - unregisterEventHandler(id); - }); + 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) => { - return registerEventHandler(this.worklet, eventName, tag); - }); + const newRegistrations = this.eventNames.map((eventName) => + registerEventHandler(this.worklet, eventName, tag) + ); this.#registrations.set(tag, newRegistrations); }); } @@ -68,9 +67,9 @@ class WorkletEventHandlerNative registerForEvents(viewTag: number, fallbackEventName?: string): void { this.#viewTags.add(viewTag); - const newRegistrations = this.eventNames.map((eventName) => { - return registerEventHandler(this.worklet, eventName, viewTag); - }); + const newRegistrations = this.eventNames.map((eventName) => + registerEventHandler(this.worklet, eventName, viewTag) + ); this.#registrations.set(viewTag, newRegistrations); if (this.eventNames.length === 0 && fallbackEventName) { From faa8ec9f62d4910f98a1ec115cfac6ae6026d474 Mon Sep 17 00:00:00 2001 From: szydlovsky <9szydlowski9@gmail.com> Date: Wed, 10 Apr 2024 12:29:26 +0200 Subject: [PATCH 14/14] upgrade and clarify some tag logic --- .../createAnimatedComponent.tsx | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/createAnimatedComponent/createAnimatedComponent.tsx b/src/createAnimatedComponent/createAnimatedComponent.tsx index f9f9f38b875..df9687cfced 100644 --- a/src/createAnimatedComponent/createAnimatedComponent.tsx +++ b/src/createAnimatedComponent/createAnimatedComponent.tsx @@ -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); } } }