diff --git a/packages/react-native-gesture-handler/src/handlers/gestures/GestureDetector/utils.ts b/packages/react-native-gesture-handler/src/handlers/gestures/GestureDetector/utils.ts index 5b5f36c97f..5bf1ea6f44 100644 --- a/packages/react-native-gesture-handler/src/handlers/gestures/GestureDetector/utils.ts +++ b/packages/react-native-gesture-handler/src/handlers/gestures/GestureDetector/utils.ts @@ -65,20 +65,16 @@ function extractValidHandlerTags(interactionGroup: GestureRef[] | undefined) { } export function extractGestureRelations(gesture: GestureType) { - gesture.config.requireToFail = extractValidHandlerTags( - gesture.config.requireToFail - ); - gesture.config.simultaneousWith = extractValidHandlerTags( + const requireToFail = extractValidHandlerTags(gesture.config.requireToFail); + const simultaneousWith = extractValidHandlerTags( gesture.config.simultaneousWith ); - gesture.config.blocksHandlers = extractValidHandlerTags( - gesture.config.blocksHandlers - ); + const blocksHandlers = extractValidHandlerTags(gesture.config.blocksHandlers); return { - waitFor: gesture.config.requireToFail, - simultaneousHandlers: gesture.config.simultaneousWith, - blocksHandlers: gesture.config.blocksHandlers, + waitFor: requireToFail, + simultaneousHandlers: simultaneousWith, + blocksHandlers: blocksHandlers, }; } diff --git a/packages/react-native-gesture-handler/src/handlers/gestures/gesture.ts b/packages/react-native-gesture-handler/src/handlers/gestures/gesture.ts index c572b848f0..bd84d83b7b 100644 --- a/packages/react-native-gesture-handler/src/handlers/gestures/gesture.ts +++ b/packages/react-native-gesture-handler/src/handlers/gestures/gesture.ts @@ -137,6 +137,17 @@ export abstract class BaseGesture< public handlerTag = -1; public handlerName = ''; public config: BaseGestureConfig = {}; + // Snapshot of the relations defined directly on this gesture (e.g. via + // `simultaneousWithExternalGesture`), captured before any composition extends + // them. Composition rebuilds the relation config from this snapshot on every + // `prepare`, so repeated renders don't accumulate references to gestures from + // previous renders (memory leak, see #3763), while keeping the original + // references so relations stay re-resolvable after a remount, such as a + // `react-freeze` unfreeze (see #4238). + public relationsSnapshot?: { + simultaneousWith: GestureRef[] | undefined; + requireToFail: GestureRef[] | undefined; + }; public handlers: HandlerCallbacks = { gestureId: -1, handlerTag: -1, diff --git a/packages/react-native-gesture-handler/src/handlers/gestures/gestureComposition.ts b/packages/react-native-gesture-handler/src/handlers/gestures/gestureComposition.ts index 56f3dfd312..d4daf4d9fb 100644 --- a/packages/react-native-gesture-handler/src/handlers/gestures/gestureComposition.ts +++ b/packages/react-native-gesture-handler/src/handlers/gestures/gestureComposition.ts @@ -31,16 +31,29 @@ export class ComposedGesture extends Gesture { requireGesturesToFail: GestureType[] ) { if (gesture instanceof BaseGesture) { + // Capture the relations defined directly on the gesture before composition + // extends them, then always rebuild from that snapshot. Otherwise, when the + // gesture is stable (e.g. wrapped in `useMemo`) but the composition is + // recreated on every render, the relations would keep accumulating + // references to gestures from previous renders, leaking memory (see #3763). + // We keep the original references (instead of collapsing them to handler + // tags) so relations can still be re-resolved after a remount, such as a + // `react-freeze` unfreeze (see #4238). + gesture.relationsSnapshot ??= { + simultaneousWith: gesture.config.simultaneousWith, + requireToFail: gesture.config.requireToFail, + }; + const newConfig = { ...gesture.config }; // No need to extend `blocksHandlers` here, because it's not changed in composition. // The same effect is achieved by reversing the order of 2 gestures in `Exclusive` newConfig.simultaneousWith = extendRelation( - newConfig.simultaneousWith, + gesture.relationsSnapshot.simultaneousWith, simultaneousGestures ); newConfig.requireToFail = extendRelation( - newConfig.requireToFail, + gesture.relationsSnapshot.requireToFail, requireGesturesToFail );