Fix GestureDetector unresponsive after display:none toggle (New Arch)#3964
Fix GestureDetector unresponsive after display:none toggle (New Arch)#3964janicduplessis wants to merge 1 commit intosoftware-mansion:mainfrom
Conversation
There was a problem hiding this comment.
Pull request overview
Fixes GestureDetector becoming unresponsive on Fabric after a display: 'none' hide/show cycle by reattaching gesture handlers when Fabric recycles underlying UIView instances.
Changes:
- Track the React view tag (
viewTag) each handler is bound to and clear it on unbind - Add a reattach pass (
reattachHandlersIfNeeded) executed after each UI operations flush - Add a basic-example repro screen for the
display:nonerecycling scenario
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/react-native-gesture-handler/apple/RNGestureHandlerRegistry.m | Refactors registry implementation to support exposing handler collection |
| packages/react-native-gesture-handler/apple/RNGestureHandlerRegistry.h | Exposes registry handlers collection for reattach iteration |
| packages/react-native-gesture-handler/apple/RNGestureHandlerModule.mm | Ensures UI block runs to reattach handlers after flush |
| packages/react-native-gesture-handler/apple/RNGestureHandlerManager.mm | Adds helper for binding + reattach pass over registered handlers |
| packages/react-native-gesture-handler/apple/RNGestureHandlerManager.h | Exposes reattachHandlersIfNeeded on manager |
| packages/react-native-gesture-handler/apple/RNGestureHandler.mm | Stores viewTag when binding and clears it on unbind |
| packages/react-native-gesture-handler/apple/RNGestureHandler.h | Adds viewTag property to handler |
| apps/basic-example/src/DisplayNone.tsx | Adds a repro/testing screen for the Fabric display:none issue |
| apps/basic-example/src/App.tsx | Registers the new example screen in the example list |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| - (void)reattachHandlersIfNeeded | ||
| { | ||
| // Re-bind handlers to their current native views. On Fabric, when a parent view has | ||
| // display:none and siblings change, the native UIView backing a component may be recycled | ||
| // and replaced. maybeBindHandler is a no-op if the view is nil or unchanged. | ||
| for (RNGestureHandler *handler in _registry.handlers.objectEnumerator) { | ||
| if (handler.viewTag == nil) { | ||
| continue; | ||
| } | ||
|
|
||
| [self maybeBindHandler:handler.tag | ||
| toViewWithTag:handler.viewTag | ||
| withActionType:handler.actionType | ||
| withHostDetector:nil]; | ||
| } | ||
| } |
There was a problem hiding this comment.
This enumerates _registry.handlers while calling maybeBindHandler, which in turn calls into registry attach logic (attachHandlerWithTag:...). If the registry mutates its handlers dictionary as part of (re)attach, this risks a “mutated while being enumerated” crash. Safer approach: iterate over a snapshot (e.g., _registry.handlers.allValues captured into an array before the loop, or iterate over a copied dictionary’s values).
There was a problem hiding this comment.
This is safe because attachHandlerWithTag:toView:withActionType:withHostDetector: in the registry only mutates existing entries (calls unbindFromView/bindToView on a handler that's already in the dictionary). It never adds or removes keys from _handlers, so the dictionary is not structurally mutated during enumeration.
There was a problem hiding this comment.
Using objectEnumerator here instead of allValues to avoid allocating a temporary array on every flush.
packages/react-native-gesture-handler/apple/RNGestureHandlerRegistry.h
Outdated
Show resolved
Hide resolved
7a0834f to
a3b6f36
Compare
…New Arch) On New Architecture, when a parent view has display:none and siblings change, the native UIView backing a component may be recycled and replaced while the React view tag stays the same. Gesture recognizers attached to the old UIView are lost, making GestureDetector unresponsive. This adds a reattachHandlersIfNeeded check in flushOperations that re-binds handlers whose native view has changed. The check is a fast no-op (pointer comparison) when views haven't been recycled. Fixes software-mansion#3937
a3b6f36 to
dc71350
Compare
|
The reproduction seems to work without the changes from the PR: Simulator.Screen.Recording.-.iPhone.17.Pro.-.2026-02-09.at.11.09.49.movOr am I missing something? |
|
@j-piasecki Sorry I oversimplified the repro, I updated it and tested it works. |
|
Hey, sorry for the delay! This should be fixed for the hook-based API by #3967. Can I ask you to double-check that? If it already works in the V3 API, can you gate the logic in this PR so it doesn't run for V3? You can use this method to check if the gesture is being used as part of the new API. |
Description
On New Architecture, when a parent view has
display: 'none'and siblings change while hidden, the nativeUIViewbacking a component may be recycled and replaced. The React view tag stays the same but theUIViewinstance changes, causing gesture recognizers to be lost.This adds a
reattachHandlersIfNeededcheck influshOperationsthat re-binds handlers whose native view has changed. The check is a fast no-op (pointer comparison) when views haven't been recycled.Fixes #3937
Test plan
Tested with both v2 (
Gesture.Tap()) and v3 (useTapGesture) APIs on iOS simulator (New Architecture).Minimal repro to verify the fix:
Steps: