Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 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(
Comment thread
szydlovsky marked this conversation as 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);
Comment thread
szydlovsky marked this conversation as 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>>;

Comment thread
szydlovsky marked this conversation as 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[] = []
Comment thread
szydlovsky marked this conversation as resolved.
Outdated
) {
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) => {
Comment thread
szydlovsky marked this conversation as resolved.
Outdated
registrationIDs.forEach((id) => {
unregisterEventHandler(id);
});
Comment thread
szydlovsky marked this conversation as resolved.
Outdated
// 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);
});
Comment thread
szydlovsky marked this conversation as resolved.
Outdated
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);
});
Comment thread
szydlovsky marked this conversation as resolved.
Outdated
this.#registrations.set(viewTag, newRegistrations);

if (this.eventNames.length === 0 && fallbackEventName) {
Comment thread
szydlovsky marked this conversation as 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
26 changes: 21 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,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) {

Copy link
Copy Markdown
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
Copy Markdown
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