Skip to content

fix: stop losing drag activations that no gesture callback will recover - #610

Draft
MatiPl01 wants to merge 5 commits into
mainfrom
fix/drag-cleanup-on-gesture-finalize
Draft

fix: stop losing drag activations that no gesture callback will recover#610
MatiPl01 wants to merge 5 commits into
mainfrom
fix/drag-cleanup-on-gesture-finalize

Conversation

@MatiPl01

@MatiPl01 MatiPl01 commented Jul 27, 2026

Copy link
Copy Markdown
Owner

This PR started from the assumption that onFinalize always runs when a drag terminates, so wiring cleanup to it would cover the cases the touch callbacks miss. That assumption is wrong. Measured on iOS with gesture-handler 3.0.2:

gesture callbacks delivered
tap onTouchesDown, onTouchesUp
press and release without moving onTouchesDown, onTouchesUp
drag and release onTouchesDown, onTouchesUp, onFinalize
drag past the fail offset onTouchesDown, onTouchesCancelled, onFinalize

A touch that ends before the item activates never reaches onFinalize, and a handler detached mid-gesture reaches no terminal state and emits nothing at all. onFinalize stays as a safety net for terminations the touch callbacks miss, but it cannot be the only cleanup path: moving cleanup out of the touch callbacks leaves a tapped item lifted with no finger down and the container unable to sort.

Looking for what actually strands the container turned up three activation paths that die leaving nothing on screen to show for it, which matches reports of tiles that stop responding after a re-render while still rendering normally:

  • a handler re-registering mid-touch replays onTouchesDown for the finger already down, and re-arming the activation timeout on every replay pushed activation out of reach, so the item never lifted;
  • the pending activation lived in a single container-wide slot that any item could clear, so a sibling ending its own gesture revoked the activation the finger was waiting on. Adding onFinalize runs handleDragEnd on every failed touch, which made this far more frequent;
  • the activation timeout returned without failing the gesture when the item had no measured position or dimensions.

The pending activation now has an owner, so only that item can cancel it. A replayed touch stream neither restarts nor kills a drag in progress. The timeout fails the gesture instead of returning silently. Activation state that outlived the drag that created it is discarded on the next touch. The v3 gesture config is also memoized so it reaches the native handler once instead of on every render.

dragActivationRecovery.test.tsx covers these, six of its cases failing without the change. Verified on the iOS simulator on the new architecture with gesture-handler v3: consecutive drags, drags after re-render-forcing data changes, and custom drag handles all sort, and a tap leaves the grid untouched.

Known limitation: every failure here comes from gesture-handler not delivering a callback, and nothing in JS can recover a handler that is detached and never re-attached.

Drag cleanup ran only from the onTouchesUp and onTouchesCancelled touch
callbacks. Those are not guaranteed to arrive when a drag ends for a reason
other than the user lifting a finger, such as another gesture winning or the
handler being cancelled. When they are skipped, handleDragEnd never runs, so
the item keeps a non-zero activation progress and stays the active item, and
the activation gate then refuses to start a drag on it ever again.

Wire the cleanup to onFinalize, which gesture-handler dispatches from handler
state transitions for END, FAILED and CANCELLED alike. The touch callbacks
keep running the same cleanup so a touch released before the activation delay
still cancels its pending activation timeout.
@vercel

vercel Bot commented Jul 27, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
react-native-sortables-docs Ignored Ignored Preview Jul 27, 2026 12:42pm

@MatiPl01 MatiPl01 self-assigned this Jul 27, 2026
MatiPl01 added 3 commits July 27, 2026 09:13
onFinalize covers a drag that ends without the user lifting a finger, but a
handler detached mid-gesture reaches no terminal state and emits no callback
at all, so cleanup cannot depend on it alone. Three activation paths could
silently strand the container with nothing on screen to show for it:

- a handler re-registering mid-touch replayed onTouchesDown, and re-arming the
  activation timeout on every replay pushed activation out of reach, so the
  item never lifted;
- the pending activation lived in a single container-wide slot that any item
  could clear, so a sibling ending its own gesture revoked the activation the
  finger was waiting on - onFinalize made that far more frequent;
- the activation timeout returned without failing the gesture when the item had
  no measured position or dimensions.

Give the pending activation an owner so only that item can cancel it, keep a
replayed touch stream from restarting or killing a drag, fail the gesture
instead of returning silently, and discard activation state that outlived the
drag that created it.

Also memoize the v3 gesture config so it is pushed to the native handler once
rather than on every render.
@MatiPl01 MatiPl01 changed the title fix: clean up drag state when the item gesture finalizes fix: stop losing drag activations that no gesture callback will recover Jul 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant