Skip to content

Commit 255c72c

Browse files
authored
Simplify VirtualDetector/InterceptingGestureDetector and reduce the number of renders (#3813)
## Description The communication logic between `VirtualDetector`/`InterceptingGestureDetector` was complex, since it was passing callbacks through ref and everything else through state via context. Since we're relying on Reanimated's `useEvent` and `useComposedEventHandler`, we need to trigger a rerender anyway to update the handler. This means there's no point in trying to optimize passing callbacks via refs, as we need to end with a render, and synchronizing renders and state updates requires additional logic. This PR simplifies the communication layer to pass everything through the state, which should greatly simplify logic. It also: - changes behavior of `shouldUseReanimated`, `dispatchesAnimatedEvents` flags - now it checks every virtual gesture, where previously the last registered one was the deciding factor - explicitly disables auto-memoization for `VirtualDetector` so that children changes can be detected - this should be changed to use MutationObserver once it's rolled out in RN - adds manual memoization to `InterceptingGestureDetector` ## Test plan Tested on the following snippet: ``` import * as React from 'react'; import { Button, StyleSheet, Text, View } from 'react-native'; import { GestureDetector, InterceptingGestureDetector, useTap, } from 'react-native-gesture-handler'; import { COLORS } from './colors'; function TextWithTap() { const tap = useTap({ onStart: () => { 'worklet'; console.log('Tapped on text in its own component!'); }, }); return ( <GestureDetector gesture={tap}> <Text style={{ fontSize: 24, color: COLORS.KINDA_GREEN }}> This text is rendered in a separate component. </Text> </GestureDetector> ); } function NativeDetectorExample() { const [entireVisible, setEntireVisible] = React.useState(true); const [firstVisible, setFirstVisible] = React.useState(true); const [secondVisible, setSecondVisible] = React.useState(true); const [thirdVisible, setThirdVisible] = React.useState(true); const [secondKey, setSecondKey] = React.useState(0); const [firstHasCallback, setFirstHasCallback] = React.useState(true); const tapAll = useTap({ onStart: () => { 'worklet'; console.log('Tapped on text!'); }, }); const tapFirstPart = useTap({ onStart: firstHasCallback ? () => { 'worklet'; console.log('Tapped on first part!'); } : () => { 'worklet'; console.log('First part tapped, but no callback set.'); }, }); const tapSecondPart = useTap({ onStart: () => { 'worklet'; console.log('Tapped on second part!'); }, }); return ( <View style={styles.subcontainer}> <Button title={(firstVisible ? 'Hide' : 'Show') + ' entire text'} onPress={() => setEntireVisible((v) => !v)} /> <Button title={(firstVisible ? 'Hide' : 'Show') + ' first part'} onPress={() => setFirstVisible((v) => !v)} /> <Button title={(secondVisible ? 'Hide' : 'Show') + ' second part'} onPress={() => setSecondVisible((v) => !v)} /> <Button title={(thirdVisible ? 'Hide' : 'Show') + ' third part'} onPress={() => setThirdVisible((v) => !v)} /> <Button title="Re-mount second part" onPress={() => setSecondKey((k) => k + 1)} /> <Button title={ (firstHasCallback ? 'Disable' : 'Enable') + ' callback on first text' } onPress={() => setFirstHasCallback((v) => !v)} /> {entireVisible && ( <InterceptingGestureDetector gesture={tapAll}> <Text style={{ fontSize: 18, textAlign: 'center' }}> Some text example running with RNGH {firstVisible && ( <GestureDetector gesture={tapFirstPart}> <Text style={{ fontSize: 24, color: COLORS.NAVY }}> {' '} try tapping on this part </Text> </GestureDetector> )} {secondVisible && ( <GestureDetector gesture={tapSecondPart}> <Text key={secondKey} style={{ fontSize: 28, color: COLORS.KINDA_BLUE }}> {' '} or on this part </Text> </GestureDetector> )} {thirdVisible && ( <> {' '} <TextWithTap /> </> )}{' '} this part is not special :( </Text> </InterceptingGestureDetector> )} </View> ); } export default function NativeTextExample() { return ( <View style={styles.container}> <NativeDetectorExample /> </View> ); } const styles = StyleSheet.create({ container: { flex: 1, }, subcontainer: { flex: 1, gap: 8, alignItems: 'center', justifyContent: 'center', }, header: { fontSize: 18, textAlign: 'center', paddingHorizontal: 24, }, }); ```
1 parent daa841d commit 255c72c

File tree

7 files changed

+216
-152
lines changed

7 files changed

+216
-152
lines changed

packages/react-native-gesture-handler/apple/RNGestureHandlerDetector.mm

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ - (void)attachHandlers:(const std::vector<int> &)handlerTags
223223
}
224224

225225
// This covers the case where `NativeViewGestureHandlers` are attached after child views were created.
226-
if (self.subviews[0]) {
226+
if (self.subviews.count != 0) {
227227
[self tryAttachNativeHandlersToChildView];
228228
}
229229
}

packages/react-native-gesture-handler/src/v3/detectors/GestureDetector.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
GestureDetectorProps as LegacyGestureDetectorProps,
77
GestureDetector as LegacyGestureDetector,
88
} from '../../handlers/gestures/GestureDetector';
9-
import { DetectorContext } from './VirtualDetector/useDetectorContext';
9+
import { InterceptingDetectorContext } from './VirtualDetector/useInterceptingDetectorContext';
1010
import { VirtualDetector } from './VirtualDetector/VirtualDetector';
1111
import { use } from 'react';
1212
import { isTestEnv } from '../../utils';
@@ -31,7 +31,7 @@ export function GestureDetector<THandlerData, TConfig>(
3131
return <LegacyGestureDetector {...(props as LegacyGestureDetectorProps)} />;
3232
}
3333

34-
const context = use(DetectorContext);
34+
const context = use(InterceptingDetectorContext);
3535
return context ? (
3636
<VirtualDetector
3737
{...(props as NativeDetectorProps<THandlerData, TConfig>)}
Lines changed: 142 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
1-
import React, { RefObject, useCallback, useRef, useState } from 'react';
1+
import React, { useCallback, useEffect, useMemo, useState } from 'react';
22
import HostGestureDetector from '../HostGestureDetector';
33
import {
4-
VirtualChildren,
4+
VirtualChild,
55
GestureHandlerEvent,
66
DetectorCallbacks,
77
} from '../../types';
8-
import { DetectorContext } from './useDetectorContext';
8+
import {
9+
InterceptingDetectorContext,
10+
InterceptingDetectorContextValue,
11+
InterceptingDetectorMode,
12+
} from './useInterceptingDetectorContext';
913
import { Reanimated } from '../../../handlers/gestures/reanimatedWrapper';
1014
import { configureRelations, ensureNativeDetectorComponent } from '../utils';
1115
import { isComposedGesture } from '../../hooks/utils/relationUtils';
@@ -17,64 +21,98 @@ import {
1721
} from '../common';
1822
import { tagMessage } from '../../../utils';
1923

24+
interface VirtualChildrenForNative {
25+
viewTag: number;
26+
handlerTags: number[];
27+
viewRef: unknown;
28+
}
29+
2030
export function InterceptingGestureDetector<THandlerData, TConfig>({
2131
gesture,
2232
children,
2333
}: InterceptingGestureDetectorProps<THandlerData, TConfig>) {
24-
const [virtualChildren, setVirtualChildren] = useState<VirtualChildren[]>([]);
25-
26-
const virtualMethods = useRef<
27-
Map<number, RefObject<DetectorCallbacks<unknown>>>
28-
>(new Map());
29-
30-
const [shouldUseReanimated, setShouldUseReanimated] = useState(
31-
gesture ? gesture.config.shouldUseReanimatedDetector : false
34+
const [virtualChildren, setVirtualChildren] = useState<Set<VirtualChild>>(
35+
() => new Set()
3236
);
33-
const [dispatchesAnimatedEvents, setDispatchesAnimatedEvents] = useState(
34-
gesture ? gesture.config.dispatchesAnimatedEvents : false
37+
const virtualChildrenForNativeComponent: VirtualChildrenForNative[] = useMemo(
38+
() =>
39+
Array.from(virtualChildren).map((child) => ({
40+
viewTag: child.viewTag,
41+
handlerTags: child.handlerTags,
42+
viewRef: child.viewRef,
43+
})),
44+
[virtualChildren]
3545
);
46+
const [mode, setMode] = useState<InterceptingDetectorMode>(
47+
gesture?.config.shouldUseReanimatedDetector
48+
? InterceptingDetectorMode.REANIMATED
49+
: gesture?.config.dispatchesAnimatedEvents
50+
? InterceptingDetectorMode.ANIMATED
51+
: InterceptingDetectorMode.DEFAULT
52+
);
53+
54+
const shouldUseReanimatedDetector =
55+
mode === InterceptingDetectorMode.REANIMATED;
56+
const dispatchesAnimatedEvents = mode === InterceptingDetectorMode.ANIMATED;
3657

3758
const NativeDetectorComponent = dispatchesAnimatedEvents
3859
? AnimatedNativeDetector
39-
: shouldUseReanimated
60+
: shouldUseReanimatedDetector
4061
? ReanimatedNativeDetector
4162
: HostGestureDetector;
4263

43-
const register = useCallback(
44-
(
45-
child: VirtualChildren,
46-
methods: RefObject<DetectorCallbacks<unknown>>,
47-
forReanimated: boolean | undefined,
48-
forAnimated: boolean | undefined
49-
) => {
50-
setShouldUseReanimated(!!forReanimated);
51-
setDispatchesAnimatedEvents(!!forAnimated);
52-
53-
setVirtualChildren((prev) => {
54-
const index = prev.findIndex((c) => c.viewTag === child.viewTag);
55-
if (index !== -1) {
56-
const updated = [...prev];
57-
updated[index] = child;
58-
return updated;
59-
}
64+
const register = useCallback((child: VirtualChild) => {
65+
setVirtualChildren((prev) => {
66+
const newSet = new Set(prev);
67+
newSet.add(child);
68+
return newSet;
69+
});
70+
}, []);
6071

61-
return [...prev, child];
62-
});
72+
const unregister = useCallback((child: VirtualChild) => {
73+
setVirtualChildren((prev) => {
74+
const newSet = new Set(prev);
75+
newSet.delete(child);
76+
return newSet;
77+
});
78+
}, []);
6379

64-
child.handlerTags.forEach((tag) => {
65-
virtualMethods.current.set(tag, methods);
66-
});
67-
},
68-
[]
69-
);
80+
const contextValue: InterceptingDetectorContextValue = useMemo(
81+
() => ({
82+
mode,
83+
setMode: (newMode: InterceptingDetectorMode) => {
84+
if (
85+
(newMode === InterceptingDetectorMode.REANIMATED &&
86+
mode === InterceptingDetectorMode.ANIMATED) ||
87+
(newMode === InterceptingDetectorMode.ANIMATED &&
88+
mode === InterceptingDetectorMode.REANIMATED)
89+
) {
90+
throw new Error(
91+
tagMessage(
92+
'InterceptingGestureDetector can only handle either Reanimated or Animated events.'
93+
)
94+
);
95+
}
7096

71-
const unregister = useCallback((childTag: number, handlerTags: number[]) => {
72-
handlerTags.forEach((tag) => {
73-
virtualMethods.current.delete(tag);
74-
});
97+
setMode(newMode);
98+
},
99+
register,
100+
unregister,
101+
}),
102+
[mode, register, unregister]
103+
);
75104

76-
setVirtualChildren((prev) => prev.filter((c) => c.viewTag !== childTag));
77-
}, []);
105+
useEffect(() => {
106+
if (gesture?.config?.dispatchesAnimatedEvents) {
107+
contextValue.setMode(InterceptingDetectorMode.ANIMATED);
108+
} else if (gesture?.config?.shouldUseReanimatedDetector) {
109+
contextValue.setMode(InterceptingDetectorMode.REANIMATED);
110+
}
111+
}, [
112+
contextValue,
113+
gesture?.config?.dispatchesAnimatedEvents,
114+
gesture?.config?.shouldUseReanimatedDetector,
115+
]);
78116

79117
// It might happen only with ReanimatedNativeDetector
80118
if (!NativeDetectorComponent) {
@@ -85,20 +123,23 @@ export function InterceptingGestureDetector<THandlerData, TConfig>({
85123
);
86124
}
87125

88-
const handleGestureEvent = (key: keyof DetectorCallbacks<THandlerData>) => {
89-
return (e: GestureHandlerEvent<THandlerData>) => {
90-
if (gesture?.detectorCallbacks[key]) {
91-
gesture.detectorCallbacks[key](e);
92-
}
93-
94-
virtualMethods.current.forEach((ref) => {
95-
const method = ref.current?.[key];
96-
if (method) {
97-
method(e);
126+
const createGestureEventHandler = useCallback(
127+
(key: keyof DetectorCallbacks<THandlerData>) => {
128+
return (e: GestureHandlerEvent<THandlerData>) => {
129+
if (gesture?.detectorCallbacks[key]) {
130+
gesture.detectorCallbacks[key](e);
98131
}
99-
});
100-
};
101-
};
132+
133+
virtualChildren.forEach((child) => {
134+
const method = child.methods[key];
135+
if (method) {
136+
method(e);
137+
}
138+
});
139+
};
140+
},
141+
[gesture, virtualChildren]
142+
);
102143

103144
const getHandlers = useCallback(
104145
(key: keyof DetectorCallbacks<unknown>) => {
@@ -112,8 +153,8 @@ export function InterceptingGestureDetector<THandlerData, TConfig>({
112153
);
113154
}
114155

115-
virtualMethods.current.forEach((ref) => {
116-
const handler = ref.current?.[key];
156+
virtualChildren.forEach((child) => {
157+
const handler = child.methods[key];
117158
if (handler) {
118159
handlers.push(
119160
handler as (e: GestureHandlerEvent<THandlerData>) => void
@@ -126,14 +167,28 @@ export function InterceptingGestureDetector<THandlerData, TConfig>({
126167
[virtualChildren, gesture?.detectorCallbacks]
127168
);
128169

170+
const reanimatedUpdateEvents = useMemo(
171+
() => getHandlers('onReanimatedUpdateEvent'),
172+
[getHandlers]
173+
);
129174
const reanimatedEventHandler = Reanimated?.useComposedEventHandler(
130-
getHandlers('onReanimatedUpdateEvent')
175+
reanimatedUpdateEvents
176+
);
177+
178+
const reanimatedStateChangeEvents = useMemo(
179+
() => getHandlers('onReanimatedStateChange'),
180+
[getHandlers]
131181
);
132182
const reanimatedStateChangeHandler = Reanimated?.useComposedEventHandler(
133-
getHandlers('onReanimatedStateChange')
183+
reanimatedStateChangeEvents
184+
);
185+
186+
const reanimatedTouchEvents = useMemo(
187+
() => getHandlers('onReanimatedTouchEvent'),
188+
[getHandlers]
134189
);
135190
const reanimatedTouchEventHandler = Reanimated?.useComposedEventHandler(
136-
getHandlers('onReanimatedTouchEvent')
191+
reanimatedTouchEvents
137192
);
138193

139194
ensureNativeDetectorComponent(NativeDetectorComponent);
@@ -142,47 +197,53 @@ export function InterceptingGestureDetector<THandlerData, TConfig>({
142197
configureRelations(gesture);
143198
}
144199

200+
const handlerTags = useMemo(() => {
201+
if (gesture) {
202+
return isComposedGesture(gesture) ? gesture.tags : [gesture.tag];
203+
}
204+
return [];
205+
}, [gesture]);
206+
145207
return (
146-
<DetectorContext value={{ register, unregister }}>
208+
<InterceptingDetectorContext value={contextValue}>
147209
<NativeDetectorComponent
148210
// @ts-ignore This is a type mismatch between RNGH types and RN Codegen types
149-
onGestureHandlerStateChange={handleGestureEvent(
150-
'onGestureHandlerStateChange'
211+
onGestureHandlerStateChange={useMemo(
212+
() => createGestureEventHandler('onGestureHandlerStateChange'),
213+
[createGestureEventHandler]
151214
)}
152215
// @ts-ignore This is a type mismatch between RNGH types and RN Codegen types
153-
onGestureHandlerEvent={handleGestureEvent('onGestureHandlerEvent')}
216+
onGestureHandlerEvent={useMemo(
217+
() => createGestureEventHandler('onGestureHandlerEvent'),
218+
[createGestureEventHandler]
219+
)}
154220
// @ts-ignore This is a type mismatch between RNGH types and RN Codegen types
155221
onGestureHandlerAnimatedEvent={
156222
gesture?.detectorCallbacks.onGestureHandlerAnimatedEvent
157223
}
158224
// @ts-ignore This is a type mismatch between RNGH types and RN Codegen types
159-
onGestureHandlerTouchEvent={handleGestureEvent(
160-
'onGestureHandlerTouchEvent'
225+
onGestureHandlerTouchEvent={useMemo(
226+
() => createGestureEventHandler('onGestureHandlerTouchEvent'),
227+
[createGestureEventHandler]
161228
)}
162229
// @ts-ignore This is a type mismatch between RNGH types and RN Codegen types
163230
onGestureHandlerReanimatedStateChange={
164-
shouldUseReanimated ? reanimatedStateChangeHandler : undefined
231+
shouldUseReanimatedDetector ? reanimatedStateChangeHandler : undefined
165232
}
166233
// @ts-ignore This is a type mismatch between RNGH types and RN Codegen types
167234
onGestureHandlerReanimatedEvent={
168-
shouldUseReanimated ? reanimatedEventHandler : undefined
235+
shouldUseReanimatedDetector ? reanimatedEventHandler : undefined
169236
}
170237
// @ts-ignore This is a type mismatch between RNGH types and RN Codegen types
171238
onGestureHandlerReanimatedTouchEvent={
172-
shouldUseReanimated ? reanimatedTouchEventHandler : undefined
173-
}
174-
handlerTags={
175-
gesture
176-
? isComposedGesture(gesture)
177-
? gesture.tags
178-
: [gesture.tag]
179-
: []
239+
shouldUseReanimatedDetector ? reanimatedTouchEventHandler : undefined
180240
}
241+
handlerTags={handlerTags}
181242
style={nativeDetectorStyles.detector}
182-
virtualChildren={virtualChildren}
243+
virtualChildren={virtualChildrenForNativeComponent}
183244
moduleId={globalThis._RNGH_MODULE_ID}>
184245
{children}
185246
</NativeDetectorComponent>
186-
</DetectorContext>
247+
</InterceptingDetectorContext>
187248
);
188249
}

0 commit comments

Comments
 (0)