Skip to content

Fix gesture relations after freeze#4244

Open
m-bert wants to merge 1 commit into
mainfrom
@mbert/frozen-gestures
Open

Fix gesture relations after freeze#4244
m-bert wants to merge 1 commit into
mainfrom
@mbert/frozen-gestures

Conversation

@m-bert

@m-bert m-bert commented Jun 8, 2026

Copy link
Copy Markdown
Collaborator

Description

#3763 fixed memory leak where gestures would keep stale references in their relations array. However, this approach introduced a bug where relations wouldn't be applied after freezing (like freezeOnBlur from react-native-screens). This PR addresses this issue by fixing incorrectly applied relations without re-introducing memory-leaks.

To do so, we create snapshot of relations at first render. It is later used to prepare relations during next renders.

Fixes #4238

Test plan

Tested on the following reproduction
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import React from 'react';
import { StyleSheet, View } from 'react-native';
import {
  Gesture,
  GestureDetector,
  GestureHandlerRootView,
} from 'react-native-gesture-handler';
import { enableFreeze } from 'react-native-screens';

enableFreeze(true);

type RootStackParamList = {
  Tabs: undefined;
};

type TabParamList = {
  TabOne: undefined;
  TabTwo: undefined;
};

const Stack = createNativeStackNavigator<RootStackParamList>();
const Tab = createBottomTabNavigator<TabParamList>();

function TabOneScreen() {
  const outerTap = Gesture.Tap().onStart(() => void console.log('outer tap'));
  const innerTap = Gesture.Tap()
    .onStart(() => void console.log('inner tap'))
    .simultaneousWithExternalGesture(outerTap);

  return (
    <View style={styles.screen1}>
      <GestureDetector gesture={outerTap}>
        <GestureDetector gesture={innerTap}>
          <View style={{ width: 100, height: 100, backgroundColor: 'red' }} />
        </GestureDetector>
      </GestureDetector>
    </View>
  );
}

function TabTwoScreen() {
  return <View style={styles.screen2} />;
}

function TabsScreen() {
  return (
    <Tab.Navigator>
      <Tab.Screen name="TabOne" component={TabOneScreen} />
      <Tab.Screen name="TabTwo" component={TabTwoScreen} />
    </Tab.Navigator>
  );
}

const App = () => {
  return (
    <GestureHandlerRootView style={styles.root}>
      <NavigationContainer>
        <Stack.Navigator>
          <Stack.Screen
            name="Tabs"
            component={TabsScreen}
            options={{ headerShown: false }}
          />
        </Stack.Navigator>
      </NavigationContainer>
    </GestureHandlerRootView>
  );
};

export default App;

const styles = StyleSheet.create({
  root: {
    flex: 1,
  },
  screen1: {
    flex: 1,
    backgroundColor: '#f9f',
  },
  screen2: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center',
    gap: 12,
  },
  listContent: {
    paddingHorizontal: 16,
    paddingBottom: 24,
  },
  listItem: {
    paddingVertical: 16,
    borderBottomWidth: 1,
    borderBottomColor: '#ddd',
  },
});

Copilot AI review requested due to automatic review settings June 8, 2026 14:21

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes a regression where gesture relations (simultaneousWith, requireToFail, blocksHandlers) could stop working after a freeze/unfreeze cycle (e.g. freezeOnBlur from react-native-screens). It does so by avoiding in-place mutation of gesture.config when extracting relations, and by introducing a per-gesture snapshot of “direct” relations so composed gestures can rebuild relations each render without accumulating stale references (preventing the memory leak addressed in #3763).

Changes:

  • Stop mutating gesture.config inside extractGestureRelations, returning extracted handler tags without overwriting the original relation refs.
  • Add relationsSnapshot to BaseGesture to preserve the “directly defined” relation refs across composition and remounts.
  • Update ComposedGesture.prepareSingleGesture to rebuild relation arrays from relationsSnapshot on each prepare, preventing accumulation across renders.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.

File Description
packages/react-native-gesture-handler/src/handlers/gestures/GestureDetector/utils.ts Makes relation extraction side-effect free to preserve relation refs for later re-resolution.
packages/react-native-gesture-handler/src/handlers/gestures/gestureComposition.ts Rebuilds composed gesture relations from a captured snapshot to avoid stale-reference accumulation.
packages/react-native-gesture-handler/src/handlers/gestures/gesture.ts Introduces relationsSnapshot on BaseGesture to persist direct relation refs across prepares/remounts.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@m-bert m-bert marked this pull request as ready for review June 8, 2026 14:38
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.

extractGestureRelations mutates gesture config in place, causing gestures to stop responding after react-freeze unfreeze

2 participants