You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The doc is written by ai, but I get the theory, tested it, and reviewed it. After several days of experimentation with different methods I have created targeted patch in expo managed project for my own use case that works reliably, efficiently, and with minimal changes, but this is more general purpose public api proposal to close the missing gap, or even expand on it.
The problem
After the Android Touchable refactor in RNGH 3.2, Touchable presses are handled by an internal NativeViewGestureHandler.
Consider an RNGH Touchable inside a native PagerView implemented with Jetpack Compose’s HorizontalPager:
The Touchable receives DOWN and enters its pending press state.
Compose recognizes a horizontal page drag.
Compose handles the page transition, but its internal drag recognizer is not registered with RNGH.
RNGH therefore receives no competing parent gesture transition that reliably cancels the Touchable.
The Touchable may receive the terminal UP and dispatch onPress, even though the interaction was a page swipe.
Previously, React Native responder behavior could implicitly interrupt the press. After moving Touchable handling into RNGH, that implicit interoperability no longer applies in the same way.
PagerView is only one example. The general problem is interoperability between RNGH descendants and parent gestures recognized by another native gesture system.
And no, using older versions or react native pressable every time there's incompatibility is not a solution to move forward...
Summary of the proposal
RNGH currently exposes UI-thread state operations for individual gesture handlers:
These APIs require the exact target handler tag and only change that handler’s state. There is no equivalent operation for a gesture boundary to synchronously cancel the RNGH handlers participating in the same pointer stream below it.
This capability is useful when RNGH descendants are embedded inside native components whose own gesture recognizers are not represented in the RNGH orchestrator—for example:
native scroll or paging containers;
Jetpack Compose and SwiftUI components;
maps and media surfaces;
carousels implemented by third-party native SDKs;
nested native views that recognize gestures outside RNGH;
custom native components that need to invalidate pending child presses.
The proposal is to expose a native, UI-thread API for scoped descendant cancellation, with optional cancellation groups and child-side protection.
Conceptually, this provides a gesture-handler-level analogue to React Native’s pointerEvents: a boundary can prevent descendant interactions from continuing.
The important difference is that it operates synchronously inside RNGH’s native
orchestrator after a touch sequence has already begun, rather than controlling
initial React Native view hit-testing from JavaScript.
Why existing gesture relations do not solve this
RNGH provides relations such as:
simultaneousWithrequireToFailblock
These relations require both gestures to exist inside the RNGH orchestrator and require access to their handler objects or handler tags.
That is not possible when:
the parent recognizer belongs to Compose, SwiftUI, UIKit, or another native SDK;
the parent’s internal recognizer is not exposed to React Native;
descendant handler tags are private and created internally by components such as Touchable.
This does not selectively cancel descendant handlers. Activating the boundary
makes it the winning RNGH recognizer, causing the competing native-view handler
to be cancelled.
On Android, that cancellation forwards ACTION_CANCEL to the wrapped native
component and terminates its current touch stream.
Immediately ending the boundary does not restore the stream because ACTION_CANCEL is terminal. The native component cannot resume the interaction
until it receives a new DOWN, which can leave a scroll or page transition
interrupted partway through.
Adding a Pan gesture around the native component
A Pan gesture can recognize the same horizontal movement:
constpan=usePanGesture({activeOffsetX: [-8,8],});
However, this creates a second drag recognizer that competes for the same pointer stream.
In the PagerView case, the RNGH Pan can interrupt Compose’s pager, stopping the page transition partway through.
Per-page interruption driven by a UI-thread pager event
A more elaborate workaround was tested using only existing RNGH and Reanimated
APIs:
Wrap every rendered page in its own manually activated RNGH gesture.
Store each page gesture’s handler tag in a UI-thread-accessible array.
Intercept PagerView’s native onPageScroll event using Reanimated’s useEvent.
Activate and deactivate the gesture associated with the currently visible
page.
onPageScroll is a direct native event, and useEvent handles it as a worklet
without crossing the JavaScript thread.
Same thing with @expo/ui hroizontal pager which has worklet callbacks without useEvent.
Activating the page gesture causes RNGH arbitration to interrupt pending descendant Touchable handlers.
Because the actual pager drag belongs to Compose rather than RNGH, the Compose pager itself is not necessarily cancelled by this state transition.
But it relies on an indirect side effect and exploiting of 2 system limitation, and more importantly, this workaround remains timing-dependent.
A callback executing as a worklet is on the UI thread, but that does not mean it runs in the same native dispatch phase as the original pointer event.
With a controlled drag, there are usually enough intermediate movement events for the synthetic page gesture to activate before the Touchable receives UP.
With a fast flick, the native pager event may still be emitted, but it is observed at the wrong point in the interaction. The synthetic RNGH activation may happen too late to reliably interrupt the pending Touchable.
This demonstrates that UI-thread execution alone is insufficient. The operation must also express the intended effect directly.
Another problem because PagerView api is also half baked, there's no existing sync swipe gesture callback to handle the first or last page edge case, so when there's no pages to scroll onPageScroll won't trigger, and it will trigger the touchables anyway.
Proposed API
Add a synchronous UI-thread operation scoped to a boundary handler:
The list should be snapshotted because cancelling handlers may change orchestrator state while iterating.
A proof-of-concept implementation using a synchronous UI-runtime binding and native orchestrator traversal resolves the motivating issue without modifying every Touchable and without making the external native component depend directly on RNGH.
Pointer scope
Shared-pointer cancellation should be the safe default:
The boundary decides when descendant cancellation is required.
Individual gestures declaratively opt out or join cancellation groups.
The parent does not need private child handler tags.
No React context, prop drilling, or handler-tag map is required.
Native group registration
Group membership should be maintained through RNGH’s native handler lifecycle rather than through React lifecycle bookkeeping.
The orchestrator could maintain indexes such as:
cancellation group → handlers
active pointer → handlers
native boundary → handlers
Membership would be updated when:
a handler is created;
handler configuration changes;
a handler is attached or reattached;
a handler is dropped.
Cancellation would still validate:
native descendant relationship;
pointer participation;
current handler state;
group inclusion or exclusion;
protection rules.
A minimal first implementation can scan the orchestrator’s registered handlers. Since cancellation normally happens once per touch sequence, that is likely sufficient.
Native indexes can be added later if profiling shows that cancellation groups are used in unusually large gesture trees.
Another option is group-level enablement: temporarily disable a group of
handlers while an external gesture is active, then re-enable it afterward. This
would behave more like UI-thread pointerEvents than one-time cancellation.
Why this belongs in RNGH
JavaScript and React Native responder APIs can filter pointer delivery, but they cannot reliably and synchronously cancel private RNGH handlers already tracked by the native orchestrator.
Existing GestureStateManager operations require exact handler tags and affect only those handlers:
Isolated gesture root with unstable_forceActive for pages
One ridiculous solution that works reliablely with existing apis was to wrap each page or list item in its own GestureHandlerRootView with unstable_forceActive and give it a manually
controlled gesture to interrupt active touches in view when swiping on the list pages.
But it was tested briefly for the specific case. Could be other side effects and limitations.
When a horizontal swipe is detected, the manually controlled page gesture is
activated. Its purpose is to interrupt any pending or active RNGH touches inside
that page, such as Touchable presses.
It does not control the page transition or replace the pager’s own gesture. The
pager continues handling the swipe, while the temporary page gesture only tells
RNGH to cancel the page’s child gestures.
The deactivate call does not undo the cancellation. It only "rearm" to interrupt again.
In the tested setup, unstable_forceActive kept the page-local gesture handling
separate from the main PagerView gesture. Activating the page gesture cancelled
the page’s Touchables without cancelling the external pager’s touch stream.
This approach was reliable because it did not depend on the external component
emitting a scroll callback at the right time. The gesture detector received the
touch directly through RNGH, so fast flicks and slow drags followed the same
path.
However, it required a large amount of infrastructure for a simple operation:
every page or list item needed its own gesture handler;
handler tags had to be stored and cleaned up;
the active page or item had to be mapped to its handler;
the correct gesture root had to surround the correct native content;
nested roots and unstable_forceActive added platform-specific behavior;
page recycling and dynamic mounting made lifecycle management harder;
developers needed detailed knowledge of RNGH’s handler states and root
behavior.
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
The doc is written by ai, but I get the theory, tested it, and reviewed it. After several days of experimentation with different methods I have created targeted patch in expo managed project for my own use case that works reliably, efficiently, and with minimal changes, but this is more general purpose public api proposal to close the missing gap, or even expand on it.
The problem
After the Android Touchable refactor in RNGH 3.2, Touchable presses are handled by an internal
NativeViewGestureHandler.Consider an RNGH Touchable inside a native PagerView implemented with Jetpack Compose’s
HorizontalPager:DOWNand enters its pending press state.UPand dispatchonPress, even though the interaction was a page swipe.Previously, React Native responder behavior could implicitly interrupt the press. After moving Touchable handling into RNGH, that implicit interoperability no longer applies in the same way.
PagerView is only one example. The general problem is interoperability between RNGH descendants and parent gestures recognized by another native gesture system.
And no, using older versions or react native pressable every time there's incompatibility is not a solution to move forward...
Summary of the proposal
RNGH currently exposes UI-thread state operations for individual gesture handlers:
These APIs require the exact target handler tag and only change that handler’s state. There is no equivalent operation for a gesture boundary to synchronously cancel the RNGH handlers participating in the same pointer stream below it.
This capability is useful when RNGH descendants are embedded inside native components whose own gesture recognizers are not represented in the RNGH orchestrator—for example:
The proposal is to expose a native, UI-thread API for scoped descendant cancellation, with optional cancellation groups and child-side protection.
Conceptually, this provides a gesture-handler-level analogue to React Native’s
pointerEvents: a boundary can prevent descendant interactions from continuing.The important difference is that it operates synchronously inside RNGH’s native
orchestrator after a touch sequence has already begun, rather than controlling
initial React Native view hit-testing from JavaScript.
Why existing gesture relations do not solve this
RNGH provides relations such as:
These relations require both gestures to exist inside the RNGH orchestrator and require access to their handler objects or handler tags.
That is not possible when:
Approaches tested
Wrapping the native component with
NativeGestureA native component can be wrapped with:
This exposes the outer native view to RNGH, but it does not expose the native component’s internal drag recognizer.
The wrapper therefore cannot automatically participate in arbitration against a Compose or third-party recognizer.
Activating and ending the boundary handler
Another attempted workaround was to synthesize a state transition:
This does not selectively cancel descendant handlers. Activating the boundary
makes it the winning RNGH recognizer, causing the competing native-view handler
to be cancelled.
On Android, that cancellation forwards
ACTION_CANCELto the wrapped nativecomponent and terminates its current touch stream.
Immediately ending the boundary does not restore the stream because
ACTION_CANCELis terminal. The native component cannot resume the interactionuntil it receives a new
DOWN, which can leave a scroll or page transitioninterrupted partway through.
Adding a Pan gesture around the native component
A Pan gesture can recognize the same horizontal movement:
However, this creates a second drag recognizer that competes for the same pointer stream.
In the PagerView case, the RNGH Pan can interrupt Compose’s pager, stopping the page transition partway through.
Per-page interruption driven by a UI-thread pager event
A more elaborate workaround was tested using only existing RNGH and Reanimated
APIs:
onPageScrollevent using Reanimated’suseEvent.page.
Each page registers its gesture handler:
The PagerView event is intercepted directly on the UI thread:
The handler is then attached to the animated native component:
onPageScrollis a direct native event, anduseEventhandles it as a workletwithout crossing the JavaScript thread.
Same thing with @expo/ui hroizontal pager which has worklet callbacks without useEvent.
Activating the page gesture causes RNGH arbitration to interrupt pending descendant Touchable handlers.
Because the actual pager drag belongs to Compose rather than RNGH, the Compose pager itself is not necessarily cancelled by this state transition.
But it relies on an indirect side effect and exploiting of 2 system limitation, and more importantly, this workaround remains timing-dependent.
A callback executing as a worklet is on the UI thread, but that does not mean it runs in the same native dispatch phase as the original pointer event.
With a controlled drag, there are usually enough intermediate movement events for the synthetic page gesture to activate before the Touchable receives
UP.With a fast flick, the native pager event may still be emitted, but it is observed at the wrong point in the interaction. The synthetic RNGH activation may happen too late to reliably interrupt the pending Touchable.
This demonstrates that UI-thread execution alone is insufficient. The operation must also express the intended effect directly.
Another problem because PagerView api is also half baked, there's no existing sync swipe gesture callback to handle the first or last page edge case, so when there's no pages to scroll onPageScroll won't trigger, and it will trigger the touchables anyway.
Proposed API
Add a synchronous UI-thread operation scoped to a boundary handler:
The boundary handler defines:
The boundary does not need to activate or win arbitration. The external native component continues handling its own gesture.
Example:
This acts as a native gesture filter without creating a competing Pan recognizer.
Suggested native behavior
The operation could be implemented by the orchestrator approximately as follows:
The list should be snapshotted because cancelling handlers may change orchestrator state while iterating.
A proof-of-concept implementation using a synchronous UI-runtime binding and native orchestrator traversal resolves the motivating issue without modifying every Touchable and without making the external native component depend directly on RNGH.
Pointer scope
Shared-pointer cancellation should be the safe default:
This means:
It prevents the boundary from cancelling an unrelated gesture under the same subtree—for example, a gesture using another finger.
A stronger mode could optionally be supported:
This would cancel every eligible descendant regardless of pointer participation.
Cancellation groups
A general public API should allow filtering without requiring the parent to collect private child handler tags.
Handlers could declare native cancellation-group membership:
The boundary could then exclude that group:
For example, most buttons inside a native paging surface could be cancelled during a drag while one specially configured interaction remains active.
The inverse operation could also be supported:
This would cancel only descendants belonging to selected groups.
A simpler child-side opt-out could be provided as well:
This creates a two-sided model:
Native group registration
Group membership should be maintained through RNGH’s native handler lifecycle rather than through React lifecycle bookkeeping.
The orchestrator could maintain indexes such as:
Membership would be updated when:
Cancellation would still validate:
A minimal first implementation can scan the orchestrator’s registered handlers. Since cancellation normally happens once per touch sequence, that is likely sufficient.
Native indexes can be added later if profiling shows that cancellation groups are used in unusually large gesture trees.
Another option is group-level enablement: temporarily disable a group of
handlers while an external gesture is active, then re-enable it afterward. This
would behave more like UI-thread
pointerEventsthan one-time cancellation.Why this belongs in RNGH
JavaScript and React Native responder APIs can filter pointer delivery, but they cannot reliably and synchronously cancel private RNGH handlers already tracked by the native orchestrator.
Existing
GestureStateManageroperations require exact handler tags and affect only those handlers:They do not express:
The orchestrator is the only layer with authoritative access to:
A descendant-cancellation API therefore belongs at that layer.
Benefits
This API would provide:
The core proposal can remain small:
Cancellation groups, pointer scopes, and child-side protection can be introduced as compatible extensions.
All reactions