Skip to content

Fix GestureDetector unresponsive after display:none toggle (New Arch)#3964

Open
janicduplessis wants to merge 1 commit intosoftware-mansion:mainfrom
janicduplessis:@janic/fix-unresposive-gesture
Open

Fix GestureDetector unresponsive after display:none toggle (New Arch)#3964
janicduplessis wants to merge 1 commit intosoftware-mansion:mainfrom
janicduplessis:@janic/fix-unresposive-gesture

Conversation

@janicduplessis
Copy link
Contributor

@janicduplessis janicduplessis commented Feb 7, 2026

Description

On New Architecture, when a parent view has display: 'none' and siblings change while hidden, the native UIView backing a component may be recycled and replaced. The React view tag stays the same but the UIView instance changes, causing gesture recognizers to be lost.

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 #3937

Test plan

Tested with both v2 (Gesture.Tap()) and v3 (useTapGesture) APIs on iOS simulator (New Architecture).

Minimal repro to verify the fix:

function DisplayNoneTest() {
  const visible = useSharedValue(true);
  const [views, setViews] = useState(100);
  const [count, setCount] = useState(0);

  const tap = Gesture.Tap().runOnJS(true).onStart(() => setCount(c => c + 1));

  const style = useAnimatedStyle(() => ({
    display: visible.value ? 'flex' : 'none',
  }));

  const runTest = () => {
    visible.value = false;
    setTimeout(() => {
      setViews(Math.random() * 100); // trigger sibling change while hidden
      setTimeout(() => { visible.value = true; }, 500);
    }, 500);
  };

  return (
    <View>
      <Text>Tap count: {count}</Text>
      <Button title="Hide → Change → Show" onPress={runTest} />
      <Animated.View style={style}>
        {Array.from({ length: views }).map((_, i) => <View key={i} />)}
        <GestureDetector gesture={tap}>
          <View><Text>TAP</Text></View>
        </GestureDetector>
      </Animated.View>
    </View>
  );
}

Steps:

  1. Tap the box — count increments
  2. Press "Hide → Change → Show" to trigger display:none + sibling change + show
  3. Tap again — count should still increment (was broken before this fix)
  4. Repeat multiple cycles to confirm reliability

Copilot AI review requested due to automatic review settings February 7, 2026 18:05
Copy link
Contributor

Copilot AI left a 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:none recycling 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.

Comment on lines +247 to +262
- (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];
}
}
Copy link

Copilot AI Feb 7, 2026

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).

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

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.

Copy link
Contributor Author

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.

@janicduplessis janicduplessis force-pushed the @janic/fix-unresposive-gesture branch from 7a0834f to a3b6f36 Compare February 7, 2026 18:15
@janicduplessis janicduplessis changed the title Fix GestureDetector unresponsive after display:none toggle on Fabric Fix GestureDetector unresponsive after display:none toggle (New Arch) Feb 7, 2026
…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
@janicduplessis janicduplessis force-pushed the @janic/fix-unresposive-gesture branch from a3b6f36 to dc71350 Compare February 7, 2026 18:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

GestureDetector becomes unresponsive when parent has display none

1 participant