Description
When a Gesture.Hover() handler is attached on iOS, RNHoverGestureHandler adds a UIPointerInteraction to the view in bindToView:. When the handler is detached, unbindFromView tries to remove it:
- (void)unbindFromView
{
#if CHECK_TARGET(13_4)
if (@available(iOS 13.4, *)) {
[super unbindFromView];
[self.recognizer.view removeInteraction:_pointerInteraction];
}
#endif
}
[super unbindFromView] removes the gesture recognizer from the view, which sets self.recognizer.view to nil. The next line then sends removeInteraction: to nil, so it does nothing. The UIPointerInteraction is never removed from the UIView.
Once the handler is dropped, the recognizer is deallocated. UIPointerInteraction.delegate is weak, and an interaction with a nil delegate applies the system default pointer effect over the whole view. Confirmed consequences on a real iPad running iPadOS 26 (see the repro):
- A view whose detector swaps its gesture from Hover to another type keeps showing the native hover effect even though it no longer has any hover gesture (the highlight is very prominent with the Liquid Glass style).
- Fabric recycles the freed native views of unmounted hover views into unrelated components, which then show the native hover effect. In the repro this is exact: unmounting 10 hover views and adding plain
Views makes precisely the first 10 recycled targets light up.
- After hovering such a poisoned recycled view, unmounting it aborts the app: SIGABRT from the Fabric assertion in
-[RCTViewComponentView unmountChildComponentView:index:] (via RCTPerformMountInstructions). The system hover effect changes the hierarchy around the poisoned view and the next unmount transaction finds a state Fabric does not expect. So this is not only cosmetic.
Expected instead: no native hover effect on views without a hover gesture, and no crash.
Note for reproducing: Expo Go cannot show the visible symptom. It bundles RNGH 2.28.0, where the hover recognizer was still created with initWithTarget:gestureHandler (changed to self later), which keeps the interaction's delegate alive and a live delegate returning nil suppresses the effect. The interaction still leaks there, it is just invisible. Use a development build with RNGH 2.29+.
There is a one-line fix in #4291 (capture the view before calling [super unbindFromView]). It was verified against the repro: hover callbacks keep working, no stray native hover effects appear, and the crash is gone.
Steps to reproduce
Run the repro app on an iPad with Apple Pencil hover or a trackpad pointer, then either test:
Deterministic (three taps, no recycling involved):
- Hover the yellow box that has
Gesture.Hover() (it reacts via a JS callback).
- Tap "Swap to tap gesture" (the same mounted view's detector now has
Gesture.Tap(), so RNGH drops the hover handler and re-attaches).
- Hover the box again. It shows the native hover effect although it has no hover gesture anymore.
Recycling:
- Tap "Unmount hover views" (10 views with bare
Gesture.Hover() unmount).
- Tap "Add 30 targets" twice (plain
Views with no gestures, only added, never removed, so the recycle pool drains).
- Hover the numbered targets: the first 10, which received the recycled views, show the native hover effect.
- Tap "Clear" after hovering them: the app crashes.
A link to a Gist, an Expo Snack or a link to a repository based on this template that reproduces the bug.
https://github.com/petterikorpimaa/rngh-hover-leak-repro
Gesture Handler version
2.32.0 (the code is unchanged on main)
React Native version
0.86.0
Platforms
iOS
JavaScript runtime
Hermes
Workflow
Using Expo Prebuild or an Expo development build
Architecture
New Architecture (Fabric)
Build type
Debug mode
Device
Real device
Device model
iPad with Apple Pencil hover (iPadOS 26.2)
Acknowledgements
Yes
Description
When a
Gesture.Hover()handler is attached on iOS,RNHoverGestureHandleradds aUIPointerInteractionto the view inbindToView:. When the handler is detached,unbindFromViewtries to remove it:[super unbindFromView]removes the gesture recognizer from the view, which setsself.recognizer.viewto nil. The next line then sendsremoveInteraction:to nil, so it does nothing. TheUIPointerInteractionis never removed from theUIView.Once the handler is dropped, the recognizer is deallocated.
UIPointerInteraction.delegateis weak, and an interaction with a nil delegate applies the system default pointer effect over the whole view. Confirmed consequences on a real iPad running iPadOS 26 (see the repro):Views makes precisely the first 10 recycled targets light up.-[RCTViewComponentView unmountChildComponentView:index:](viaRCTPerformMountInstructions). The system hover effect changes the hierarchy around the poisoned view and the next unmount transaction finds a state Fabric does not expect. So this is not only cosmetic.Expected instead: no native hover effect on views without a hover gesture, and no crash.
Note for reproducing: Expo Go cannot show the visible symptom. It bundles RNGH 2.28.0, where the hover recognizer was still created with
initWithTarget:gestureHandler(changed toselflater), which keeps the interaction's delegate alive and a live delegate returning nil suppresses the effect. The interaction still leaks there, it is just invisible. Use a development build with RNGH 2.29+.There is a one-line fix in #4291 (capture the view before calling
[super unbindFromView]). It was verified against the repro: hover callbacks keep working, no stray native hover effects appear, and the crash is gone.Steps to reproduce
Run the repro app on an iPad with Apple Pencil hover or a trackpad pointer, then either test:
Deterministic (three taps, no recycling involved):
Gesture.Hover()(it reacts via a JS callback).Gesture.Tap(), so RNGH drops the hover handler and re-attaches).Recycling:
Gesture.Hover()unmount).Views with no gestures, only added, never removed, so the recycle pool drains).A link to a Gist, an Expo Snack or a link to a repository based on this template that reproduces the bug.
https://github.com/petterikorpimaa/rngh-hover-leak-repro
Gesture Handler version
2.32.0 (the code is unchanged on
main)React Native version
0.86.0
Platforms
iOS
JavaScript runtime
Hermes
Workflow
Using Expo Prebuild or an Expo development build
Architecture
New Architecture (Fabric)
Build type
Debug mode
Device
Real device
Device model
iPad with Apple Pencil hover (iPadOS 26.2)
Acknowledgements
Yes