Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WorkletEventHandler revamp #5845

Merged
merged 17 commits into from
Apr 17, 2024
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/createAnimatedComponent/PropsFilter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
50 changes: 26 additions & 24 deletions src/createAnimatedComponent/createAnimatedComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -254,7 +254,7 @@ export function createAnimatedComponent(
has('workletEventHandler', prop) &&
prop.workletEventHandler instanceof WorkletEventHandler
) {
prop.workletEventHandler.unregisterFromEvents();
prop.workletEventHandler.unregisterFromEvents(this._viewTag);
}
}
}
Expand All @@ -277,38 +277,40 @@ export function createAnimatedComponent(
}
}

_reattachNativeEvents(
_updateNativeEvents(
szydlovsky marked this conversation as resolved.
Show resolved Hide resolved
prevProps: AnimatedComponentProps<InitialComponentProps>
) {
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);
szydlovsky marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
Expand Down Expand Up @@ -460,7 +462,7 @@ export function createAnimatedComponent(
) {
this._configureSharedTransition();
}
this._reattachNativeEvents(prevProps);
this._updateNativeEvents(prevProps);
this._attachAnimatedStyles();
this._InlinePropManager.attachInlineProps(this, this._getViewInfo());

Expand Down
140 changes: 100 additions & 40 deletions src/reanimated2/WorkletEventHandler.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
'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<Event extends object> = NativeSyntheticEvent<EventPayload<Event>>;

szydlovsky marked this conversation as resolved.
Show resolved Hide resolved
// 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.
Expand All @@ -20,64 +23,121 @@ function jsListener<Event extends object>(
};
}

export default class WorkletEventHandler<Event extends object> {
class WorkletEventHandlerNative<Event extends object>
implements IWorkletEventHandler<Event>
{
eventNames: string[];
worklet: (event: ReanimatedEvent<Event>) => void;
#viewTags: Set<number>;
#registrations: Map<number, number[]>; // keys are viewTags, values are arrays of registration ID's for each viewTag
constructor(
worklet: (event: ReanimatedEvent<Event>) => void,
eventNames: string[] = []
szydlovsky marked this conversation as resolved.
Show resolved Hide resolved
) {
this.worklet = worklet;
this.eventNames = eventNames;
this.#viewTags = new Set<number>();
this.#registrations = new Map<number, number[]>();
}

updateEventHandler(
newWorklet: (event: ReanimatedEvent<Event>) => void,
newEvents: string[]
): void {
// Update worklet and event names
this.worklet = newWorklet;
this.eventNames = newEvents;

// Detach all events
this.#registrations.forEach((registrationIDs, _tag) => {
szydlovsky marked this conversation as resolved.
Show resolved Hide resolved
registrationIDs.forEach((id) => {
unregisterEventHandler(id);
});
szydlovsky marked this conversation as resolved.
Show resolved Hide resolved
// 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);
});
szydlovsky marked this conversation as resolved.
Show resolved Hide resolved
this.#registrations.set(tag, newRegistrations);
});
}

registerForEvents(viewTag: number, fallbackEventName?: string): void {
this.#viewTags.add(viewTag);

const newRegistrations = this.eventNames.map((eventName) => {
return registerEventHandler(this.worklet, eventName, viewTag);
});
szydlovsky marked this conversation as resolved.
Show resolved Hide resolved
this.#registrations.set(viewTag, newRegistrations);

if (this.eventNames.length === 0 && fallbackEventName) {
szydlovsky marked this conversation as resolved.
Show resolved Hide resolved
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<Event extends object>
implements IWorkletEventHandler<Event>
{
eventNames: string[];
reattachNeeded: boolean;
listeners:
| Record<string, (event: ReanimatedEvent<ReanimatedEvent<Event>>) => void>
| Record<string, (event: JSEvent<Event>) => void>;

viewTag: number | undefined;
registrations: number[];
worklet: (event: ReanimatedEvent<Event>) => void;

constructor(
worklet: (event: ReanimatedEvent<Event>) => 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<string, (event: JSEvent<Event>) => void>,
eventName: string
) => {
acc[eventName] = jsListener(eventName, worklet);
return acc;
},
{}
);
}
this.setupWebListeners();
}

updateWorklet(newWorklet: (event: ReanimatedEvent<Event>) => 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<Event>) => 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;
9 changes: 9 additions & 0 deletions src/reanimated2/hook/commonTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,15 @@ export type RNNativeScrollEvent = NativeSyntheticEvent<NativeScrollEvent>;

export type ReanimatedScrollEvent = ReanimatedEvent<RNNativeScrollEvent>;

export interface IWorkletEventHandler<Event extends object> {
updateEventHandler: (
newWorklet: (event: ReanimatedEvent<Event>) => void,
newEvents: string[]
) => void;
registerForEvents: (viewTag: number, fallbackEventName?: string) => void;
unregisterFromEvents: (viewTag: number) => void;
}

export interface AnimatedStyleHandle<
Style extends DefaultStyle = DefaultStyle
> {
Expand Down
8 changes: 4 additions & 4 deletions src/reanimated2/hook/useEvent.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -17,7 +17,7 @@ export type EventHandlerProcessed<
> = (event: Event, context?: Context) => void;

export type EventHandlerInternal<Event extends object> = {
workletEventHandler: WorkletEventHandler<Event>;
workletEventHandler: IWorkletEventHandler<Event>;
};

/**
Expand Down Expand Up @@ -57,7 +57,7 @@ export function useEvent<Event extends object, Context = never>(
initRef.current = { workletEventHandler };
} else if (rebuild) {
const workletEventHandler = initRef.current.workletEventHandler;
workletEventHandler.updateWorklet(handler);
workletEventHandler.updateEventHandler(handler, eventNames);
initRef.current = { workletEventHandler };
}

Expand Down
19 changes: 14 additions & 5 deletions src/reanimated2/hook/useScrollViewOffset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ function useScrollViewOffsetNative(
const internalOffset = useSharedValue(0);
const offset = useRef(providedOffset ?? internalOffset).current;
const scrollRef = useRef<AnimatedScrollView | null>(null);
const scrollRefTag = useRef<number | null>(null);

const eventHandler = useEvent<RNNativeScrollEvent>(
(event: ReanimatedScrollEvent) => {
Expand All @@ -89,15 +90,23 @@ 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();

const viewTag = animatedRef.getTag();
eventHandler.workletEventHandler.registerForEvents(viewTag);
eventHandler.workletEventHandler.registerForEvents(scrollRefTag.current);
szydlovsky marked this conversation as resolved.
Show resolved Hide resolved
return () => {
eventHandler.workletEventHandler.unregisterFromEvents();
if (scrollRefTag.current !== null) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note generally reading a mutable reference in effect cleanup is risky because it could've changed by the time you got to cleanup (e.g. it could've been ... cleaned up). Or replaced by a newer reference. So it's a good idea to do

useEffect(() => {
  const instance = someRef.current
  doStuff(instance)
  return () => {
    undoStuff(instance)
  })
})

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, thanks for pointing out - luckily newer versions have already made it this way 😄

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
Expand Down