Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
7 changes: 6 additions & 1 deletion packages/react-native-reanimated/src/hook/commonTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,18 @@ export interface Descriptor {
shadowNodeWrapper: ShadowNodeWrapper;
}

export type MaybeObserverCleanup = (() => void) | undefined;

export type AnimatedRefObserver = (tag: number | null) => MaybeObserverCleanup;

export type AnimatedRef<T extends Component> = {
(component?: T):
| number // Paper
| ShadowNodeWrapper // Fabric
| HTMLElement; // web
current: T | null;
getTag: () => number;
observe: (observer: AnimatedRefObserver) => void;
getTag?: () => number | null;
};

// Might make that type generic if it's ever needed.
Expand Down
62 changes: 59 additions & 3 deletions packages/react-native-reanimated/src/hook/useAnimatedRef.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@ import type { ShadowNodeWrapper } from '../commonTypes';
import { getShadowNodeWrapperFromRef } from '../fabricUtils';
import { makeMutable } from '../mutables';
import { findNodeHandle } from '../platformFunctions/findNodeHandle';
import type { AnimatedRef, AnimatedRefOnUI } from './commonTypes';
import type {
AnimatedRef,
AnimatedRefObserver,
AnimatedRefOnUI,
MaybeObserverCleanup,
} from './commonTypes';

interface MaybeScrollableComponent extends Component {
getNativeScrollRef?: FlatList['getNativeScrollRef'];
Expand All @@ -36,6 +41,9 @@ function useAnimatedRefNative<
TComponent extends Component,
>(): AnimatedRef<TComponent> {
const [tag] = useState(() => makeMutable<ShadowNodeWrapper | null>(null));
const observers = useRef<Map<AnimatedRefObserver, MaybeObserverCleanup>>(
new Map()
).current;
const tagRef = useRef<ShadowNodeWrapper | null>(null);

const ref = useRef<AnimatedRef<TComponent> | null>(null);
Expand All @@ -60,12 +68,35 @@ function useAnimatedRefNative<
// We have to unwrap the tag from the shadow node wrapper.
fun.getTag = () =>
findNodeHandle(getComponentOrScrollable(component) as Component)!;

fun.current = component;
}

if (observers.size) {
const currentTag = fun?.getTag?.() ?? null;
observers.forEach((cleanup, observer) => {
// Perform the cleanup before calling the observer again.
// This ensures that all events that were set up in the observer
// are cleaned up before the observer sets up new events during
// the next call.
cleanup?.();
observers.set(observer, observer(currentTag));
});
}

return tagRef.current;
});

fun.observe = (observer: AnimatedRefObserver) => {
// Call observer immediately to get the initial value
const cleanup = observer(fun?.getTag?.() ?? null);
observers.set(observer, cleanup);

return () => {
observers.get(observer)?.();
observers.delete(observer);
};
};

fun.current = null;

const animatedRefShareableHandle = makeShareableCloneRecursive({
Expand All @@ -85,6 +116,9 @@ function useAnimatedRefWeb<
TComponent extends Component,
>(): AnimatedRef<TComponent> {
const tagRef = useRef<ShadowNodeWrapper | null>(null);
const observers = useRef<Map<AnimatedRefObserver, MaybeObserverCleanup>>(
new Map()
).current;

const ref = useRef<AnimatedRef<TComponent> | null>(null);

Expand All @@ -103,12 +137,34 @@ function useAnimatedRefWeb<
// We have to unwrap the tag from the shadow node wrapper.
fun.getTag = () =>
findNodeHandle(getComponentOrScrollable(component) as Component)!;

fun.current = component;
}

if (observers.size) {
const currentTag = fun?.getTag?.() ?? null;
observers.forEach((cleanup, observer) => {
// Perform the cleanup before calling the observer again.
// This ensures that all events that were set up in the observer
// are cleaned up before the observer sets up new events during
// the next call.
cleanup?.();
observers.set(observer, observer(currentTag));
});
}

return tagRef.current;
});

fun.observe = (observer: AnimatedRefObserver) => {
const cleanup = observer(fun?.getTag?.() ?? null);
observers.set(observer, cleanup);

return () => {
observers.get(observer)?.();
observers.delete(observer);
};
};

fun.current = null;

ref.current = fun;
Expand Down
62 changes: 28 additions & 34 deletions packages/react-native-reanimated/src/hook/useScrollViewOffset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ import type { EventHandlerInternal } from './useEvent';
import { useEvent } from './useEvent';
import { useSharedValue } from './useSharedValue';

const NOT_INITIALIZED_WARNING =
'animatedRef is not initialized in useScrollViewOffset. Make sure to pass the animated ref to the scrollable component to get scroll offset updates.';

/**
* Lets you synchronously get the current offset of a `ScrollView`.
*
Expand Down Expand Up @@ -42,27 +45,27 @@ function useScrollViewOffsetWeb(
offset.value =
element.scrollLeft === 0 ? element.scrollTop : element.scrollLeft;
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [animatedRef, animatedRef?.current]);
}, [animatedRef, offset]);

useEffect(() => {
const element = animatedRef?.current
? getWebScrollableElement(animatedRef.current)
: null;
if (!animatedRef) {
return;
}

if (element) {
return animatedRef.observe((tag) => {
if (!tag) {
logger.warn(NOT_INITIALIZED_WARNING);
return;
}

const element = getWebScrollableElement(animatedRef.current);
element.addEventListener('scroll', eventHandler);
}
return () => {
if (element) {

return () => {
element.removeEventListener('scroll', eventHandler);
}
};
// 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
// to do it this way.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [animatedRef, animatedRef?.current, eventHandler]);
};
});
}, [animatedRef, eventHandler]);

return offset;
}
Expand Down Expand Up @@ -92,27 +95,18 @@ function useScrollViewOffsetNative(
return;
}

if (!animatedRef.getTag) {
logger.warn(
'animatedRef is not initialized. Please make sure to pass the animated ref to the scrollable component if you want to use useScrollViewOffset.'
);
return;
}

const elementTag = animatedRef.getTag();
return animatedRef.observe((tag) => {
if (!tag) {
logger.warn(NOT_INITIALIZED_WARNING);
return;
}

if (elementTag) {
eventHandler.workletEventHandler.registerForEvents(elementTag);
eventHandler.workletEventHandler.registerForEvents(tag);
return () => {
eventHandler.workletEventHandler.unregisterFromEvents(elementTag);
eventHandler.workletEventHandler.unregisterFromEvents(tag);
};
}

// 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
// to do it this way.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [animatedRef, animatedRef?.current, eventHandler]);
});
}, [animatedRef, eventHandler]);

return offset;
}
Expand Down