From 8fea848d34739bad2368d558287d7c87e96bb968 Mon Sep 17 00:00:00 2001 From: OxMarco Date: Sun, 12 Jul 2026 17:37:05 +0200 Subject: [PATCH] fix: prevent unbounded scroll-event recursion while scrollable is locked `handleOnScroll` issued `scrollTo(scrollableRef, 0, lockPosition, false)` on every scroll event while the scrollable was LOCKED, including when the scrollable was already resting at `lockPosition`. `scrollTo` still drives the underlying scroll view, which emits another scroll event, which re-enters `handleOnScroll`, which calls `scrollTo` again. Once the offset has settled there is nothing left to break the cycle, so it recurses until the JS stack overflows with `RangeError: Maximum call stack size exceeded`, with the stack alternating between Reanimated's `useAnimatedScrollHandler` and this handler. Skip the `scrollTo` when the scrollable is already at the lock position, within a small epsilon to tolerate the sub-pixel offsets reported during scrolling. `scrollableContentOffsetY.value` is still assigned unconditionally, so the shared value is unchanged. `handleOnEndDrag` and `handleOnMomentumEnd` contain the same unconditional `scrollTo`, but they fire once per gesture rather than on every scroll event, so they cannot feed themselves. Left untouched to keep this fix minimal. --- src/hooks/useScrollEventsHandlersDefault.ts | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/hooks/useScrollEventsHandlersDefault.ts b/src/hooks/useScrollEventsHandlersDefault.ts index 7b84480e..a699667f 100644 --- a/src/hooks/useScrollEventsHandlersDefault.ts +++ b/src/hooks/useScrollEventsHandlersDefault.ts @@ -13,6 +13,13 @@ export type ScrollEventContextType = { shouldLockInitialPosition: boolean; }; +/** + * Tolerance, in points, for treating the scrollable as already resting at the + * lock position. Sub-pixel offsets are reported during scrolling, so an exact + * comparison would keep re-issuing `scrollTo` and never settle. + */ +const SCROLL_LOCK_EPSILON = 0.5; + export const useScrollEventsHandlersDefault: ScrollEventsHandlersHookType = ( scrollableRef, scrollableContentOffsetY @@ -55,8 +62,17 @@ export const useScrollEventsHandlersDefault: ScrollEventsHandlersHookType = ( const lockPosition = context.shouldLockInitialPosition ? (context.initialContentOffsetY ?? 0) : 0; - // @ts-expect-error - scrollTo(scrollableRef, 0, lockPosition, false); + /** + * `scrollTo` drives the scrollable, which emits another scroll event, + * which re-enters this handler. Issuing it unconditionally, including + * when we are already sitting at `lockPosition`, makes that feedback + * loop unbounded and overflows the stack. Only correct the offset when + * it actually needs correcting. + */ + if (Math.abs(y - lockPosition) > SCROLL_LOCK_EPSILON) { + // @ts-expect-error + scrollTo(scrollableRef, 0, lockPosition, false); + } scrollableContentOffsetY.value = lockPosition; return; }