PanResponder / Responder don't appear to work on New Architecture
Problem Description
Hey folks! Have been playing around with 0.76.0 and loving it so far. One thing that doesn't seem to work, which I haven't found an existing GitHub issue for is PanResponder. I'm trying out the basic example from https://reactnative.dev/docs/panresponder and it doesn't move.
If I tweak the responder a bit to:
const panResponder = useRef(
PanResponder.create({
onStartShouldSetPanResponder: () => true,
onMoveShouldSetPanResponder: () => true,
onPanResponderGrant: () => {
console.log('onPanResponderGrant');
},
onPanResponderMove: (event, gesture) => {
console.log(gesture);
},
onPanResponderRelease: () => {
console.log('onPanResponderRelease');
pan.extractOffset();
},
}),
).current;
It'll log the onPanResponderGrant (unless the view has a <Pressable /> as a child) and onPanResponderRelease, but it never triggers onPanResponderMove
The same can be said for Gesture Responder and onResponderMove (which seems like it should work).
onPointerMove does work, so we can probably use that in the interim :)
Steps To Reproduce
Fresh install of new architecture -> copy and paste example from https://reactnative.dev/docs/panresponder
Expected Results
No response
CLI version
15.0.0
Environment
Windows 11 10.0.22631
React Native 0.76.1
RNW 0.76.0
Community Modules
No response
Target Platform Version
None
Target Device(s)
No response
Visual Studio Version
None
Build Configuration
None
Snack, code example, screenshot, or link to a repository
No response
So glad you're trying out 0.76. Thanks for the feedback! Our best guess here is that the touch events are not quite hooked up right. We'll investigate.
It seems I have the same issue on React Native 0.81.5 with Expo 54.
I also got this issue with react native 0.83.0
I looked into PanResponder code within react native and it seems like the touchHistory timestamps are only set once therefore difference in movements can't be calculated. This is workaround I came up with (someone can extract this into function if they wish).
This workaround saves reference to PanResponders inner state and uses its update functions for movement, adds multiple checks so panResponder state should stay valid (ehhh I hope so), and finally override function call to View so we can inject our own callback for movement.
function ExampleWithPanResponder()
{
const [gestureY, setGestureY] = useState(0);
const [gestureX, setGestureX] = useState(0);
const gestureState = useRef<PanResponderGestureState>({});
const updateTimeStamps = (event) => {
let currentTimestamp = performance.now() * 100;
const previousTimestamp = ev.touchHistory.mostRecentTimeStamp;
if(ev.touchHistory.mostRecentTimeStamp === currentTimestamp) {
// Potential fail safe so pan responder should always work
currentTimestamp += 1;
} else
ev.touchHistory.mostRecentTimeStamp = currentTimestamp;
ev.touchHistory.touchBank.forEach((touchRecord: any) => {
touchRecord.currentTimeStamp = ev.touchHistory.mostRecentTimeStamp;
touchRecord.previousTimeStamp = previousTimestamp;
});
};
const panResponder = useRef(
PanResponder.create({
onStartShouldSetPanResponder: () => true,
onMoveShouldSetPanResponder: () => true,
onPanResponderTerminationRequest: () => false,
onShouldBlockNativeResponder: () => true,
onPanResponderGrant: (_ev, state) => {
updateTimeStamps(ev);
gestureState.current = state;
},
onPanResponderRelease: (ev, state) => {
updateTimeStamps(ev);
},
})
).current;
return (
<View
style={{width: '100%', height: '100%', position: 'relative'}}
{...panResponder.panHandlers}
onResponderMove={(ev: GestureResponderEvent) => {
updateTimeStamps(ev);
PanResponder._updateGestureStateOnMove(gestureState.current, ev.touchHistory);
setGestureX(gestureState.current.moveX);
setGestureY(gestureState.current.moveY);
}}
>
<View style=
{{
height: 300,
width: 300,
transform: [
{ translateX: gestureX },
{ translateY: gestureY }
],
backgroundColor: 'lightblue',
position: 'absolute',
}}
/>
</View>);
}
And this is the result:
Edit:
Found the root issue. Seems like within touch history api there's use of Date.now() instead of performance.now() -> Date.now() can evaluate into same value between two frames and movement assumes to be always different. Edited code so it matches with performance.