-
-
Notifications
You must be signed in to change notification settings - Fork 1k
Fix GestureDetector unresponsive after display:none toggle (New Arch) #3964
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Fix GestureDetector unresponsive after display:none toggle (New Arch) #3964
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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]; | ||
| } | ||
| } |
Copilot
AI
Feb 7, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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. |
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: