From ecb9984588f07bee458777e0640bf4a420dc8fdf Mon Sep 17 00:00:00 2001 From: szydlovsky <9szydlowski9@gmail.com> Date: Mon, 18 Mar 2024 15:25:28 +0100 Subject: [PATCH 1/4] Add web implementation for useScrollViewOffset --- src/reanimated2/hook/useScrollViewOffset.ts | 37 +++++++++++++++++---- 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/src/reanimated2/hook/useScrollViewOffset.ts b/src/reanimated2/hook/useScrollViewOffset.ts index 879c0d99af10..9f03bdbb792b 100644 --- a/src/reanimated2/hook/useScrollViewOffset.ts +++ b/src/reanimated2/hook/useScrollViewOffset.ts @@ -1,5 +1,5 @@ 'use strict'; -import { useEffect, useRef } from 'react'; +import { useEffect, useRef, useCallback } from 'react'; import type { SharedValue } from '../commonTypes'; import { findNodeHandle } from 'react-native'; import type { EventHandlerInternal } from './useEvent'; @@ -52,20 +52,45 @@ export function useScrollViewOffset( // for more information about this cast. ) as unknown as EventHandlerInternal; + const webEventHandler = useCallback(() => { + 'worklet'; + const element = animatedRef.current as unknown as HTMLElement; + + // scrollLeft is the X axis scrolled offset, works properly also with RTL layout + offsetRef.current.value = + element.scrollLeft === 0 ? element.scrollTop : element.scrollLeft; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [animatedRef, animatedRef.current]); + + const currentEventHandler = IS_WEB ? webEventHandler : eventHandler; + useEffect(() => { const component = animatedRef.current; - const viewTag = IS_WEB ? component : findNodeHandle(component); - - eventHandler.workletEventHandler.registerForEvents(viewTag as number); + if (IS_WEB) { + (component as unknown as HTMLElement).addEventListener( + 'scroll', + webEventHandler + ); + } else { + const viewTag = findNodeHandle(component); + eventHandler.workletEventHandler.registerForEvents(viewTag as number); + } return () => { - eventHandler.workletEventHandler?.unregisterFromEvents(); + if (IS_WEB) { + (component as unknown as HTMLElement).removeEventListener( + 'scroll', + webEventHandler + ); + } else { + eventHandler.workletEventHandler?.unregisterFromEvents(); + } }; // 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, animatedRef.current, currentEventHandler]); return offsetRef.current; } From 7a473470c9320001eb2a5ee15bbe032abb68f7e3 Mon Sep 17 00:00:00 2001 From: szydlovsky <9szydlowski9@gmail.com> Date: Tue, 19 Mar 2024 11:20:39 +0100 Subject: [PATCH 2/4] Split hook into web and native versions --- src/reanimated2/hook/useScrollViewOffset.ts | 92 ++++++++++++--------- 1 file changed, 52 insertions(+), 40 deletions(-) diff --git a/src/reanimated2/hook/useScrollViewOffset.ts b/src/reanimated2/hook/useScrollViewOffset.ts index 9f03bdbb792b..b5768bbc84b9 100644 --- a/src/reanimated2/hook/useScrollViewOffset.ts +++ b/src/reanimated2/hook/useScrollViewOffset.ts @@ -15,14 +15,6 @@ import { isWeb } from '../PlatformChecker'; const IS_WEB = isWeb(); -const scrollEventNames = [ - 'onScroll', - 'onScrollBeginDrag', - 'onScrollEndDrag', - 'onMomentumScrollBegin', - 'onMomentumScrollEnd', -]; - /** * Lets you synchronously get the current offset of a `ScrollView`. * @@ -30,7 +22,52 @@ const scrollEventNames = [ * @returns A shared value which holds the current offset of the `ScrollView`. * @see https://docs.swmansion.com/react-native-reanimated/docs/scroll/useScrollViewOffset */ -export function useScrollViewOffset( +export const useScrollViewOffset = IS_WEB + ? useScrollViewOffsetJS + : useScrollViewOffsetNative; + +function useScrollViewOffsetJS( + animatedRef: AnimatedRef, + initialRef?: SharedValue +): SharedValue { + const offsetRef = useRef( + // eslint-disable-next-line react-hooks/rules-of-hooks + initialRef !== undefined ? initialRef : useSharedValue(0) + ); + + const eventHandler = useCallback(() => { + 'worklet'; + const element = animatedRef.current as unknown as HTMLElement; + // scrollLeft is the X axis scrolled offset, works properly also with RTL layout + offsetRef.current.value = + element.scrollLeft === 0 ? element.scrollTop : element.scrollLeft; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [animatedRef, animatedRef.current]); + + useEffect(() => { + const element = animatedRef.current as unknown as HTMLElement; + element.addEventListener('scroll', eventHandler); + 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]); + + return offsetRef.current; +} + +const scrollNativeEventNames = [ + 'onScroll', + 'onScrollBeginDrag', + 'onScrollEndDrag', + 'onMomentumScrollBegin', + 'onMomentumScrollEnd', +]; + +function useScrollViewOffsetNative( animatedRef: AnimatedRef, initialRef?: SharedValue ): SharedValue { @@ -47,50 +84,25 @@ export function useScrollViewOffset( ? event.contentOffset.y : event.contentOffset.x; }, - scrollEventNames + scrollNativeEventNames // Read https://github.com/software-mansion/react-native-reanimated/pull/5056 // for more information about this cast. ) as unknown as EventHandlerInternal; - const webEventHandler = useCallback(() => { - 'worklet'; - const element = animatedRef.current as unknown as HTMLElement; - - // scrollLeft is the X axis scrolled offset, works properly also with RTL layout - offsetRef.current.value = - element.scrollLeft === 0 ? element.scrollTop : element.scrollLeft; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [animatedRef, animatedRef.current]); - - const currentEventHandler = IS_WEB ? webEventHandler : eventHandler; - useEffect(() => { const component = animatedRef.current; - if (IS_WEB) { - (component as unknown as HTMLElement).addEventListener( - 'scroll', - webEventHandler - ); - } else { - const viewTag = findNodeHandle(component); - eventHandler.workletEventHandler.registerForEvents(viewTag as number); - } + const viewTag = IS_WEB ? component : findNodeHandle(component); + + eventHandler.workletEventHandler.registerForEvents(viewTag as number); return () => { - if (IS_WEB) { - (component as unknown as HTMLElement).removeEventListener( - 'scroll', - webEventHandler - ); - } else { - eventHandler.workletEventHandler?.unregisterFromEvents(); - } + eventHandler.workletEventHandler?.unregisterFromEvents(); }; // 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, currentEventHandler]); + }, [animatedRef, animatedRef.current, eventHandler]); return offsetRef.current; } From 7d2f28653ea0e192555d505ff1fcebc3214440f5 Mon Sep 17 00:00:00 2001 From: szydlovsky <9szydlowski9@gmail.com> Date: Tue, 19 Mar 2024 15:40:19 +0100 Subject: [PATCH 3/4] Avoid mem leaks from changing component ref in the meantime --- src/reanimated2/hook/useScrollViewOffset.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/reanimated2/hook/useScrollViewOffset.ts b/src/reanimated2/hook/useScrollViewOffset.ts index b5768bbc84b9..629e98377693 100644 --- a/src/reanimated2/hook/useScrollViewOffset.ts +++ b/src/reanimated2/hook/useScrollViewOffset.ts @@ -34,6 +34,7 @@ function useScrollViewOffsetJS( // eslint-disable-next-line react-hooks/rules-of-hooks initialRef !== undefined ? initialRef : useSharedValue(0) ); + const scrollRef = useRef(null); const eventHandler = useCallback(() => { 'worklet'; @@ -45,6 +46,12 @@ function useScrollViewOffsetJS( }, [animatedRef, animatedRef.current]); useEffect(() => { + // We need to make sure that listener for old animatedRef value is removed + if(scrollRef.current !== null) { + (scrollRef.current as unknown as HTMLElement).removeEventListener('scroll', eventHandler); + } + scrollRef.current = animatedRef.current; + const element = animatedRef.current as unknown as HTMLElement; element.addEventListener('scroll', eventHandler); return () => { From b8782ef4f18089b7d37c78592e9e93d26e22cf6a Mon Sep 17 00:00:00 2001 From: szydlovsky <9szydlowski9@gmail.com> Date: Tue, 19 Mar 2024 15:43:05 +0100 Subject: [PATCH 4/4] fix lint --- src/reanimated2/hook/useScrollViewOffset.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/reanimated2/hook/useScrollViewOffset.ts b/src/reanimated2/hook/useScrollViewOffset.ts index 629e98377693..2d5d4f60391a 100644 --- a/src/reanimated2/hook/useScrollViewOffset.ts +++ b/src/reanimated2/hook/useScrollViewOffset.ts @@ -47,8 +47,11 @@ function useScrollViewOffsetJS( useEffect(() => { // We need to make sure that listener for old animatedRef value is removed - if(scrollRef.current !== null) { - (scrollRef.current as unknown as HTMLElement).removeEventListener('scroll', eventHandler); + if (scrollRef.current !== null) { + (scrollRef.current as unknown as HTMLElement).removeEventListener( + 'scroll', + eventHandler + ); } scrollRef.current = animatedRef.current;