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
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: