React Native's panResponder has stale value from useState?
The pan responder depends on the value 'foo'. useRef
wouldn't be a good choice here. You should replace it with useMemo
const panResponder = useMemo(
() => PanResponder.create({
[...]
onPanResponderMove: (evt, gestureState) => {
console.log(foo); // This is now updated
},
[...]
}),
[foo] // dependency list
);
Issue
The issue is that you create the PanResponder
once with the foo
which you have at that time. However with each setFoo
call you'll receive a new foo
from the useState hook. The PanResponder wont know that new foo. This happens due to how useRef works as it provides you a mutable object which lives through your whole component lifecycle. (This is explained in the react docs here)
(You can play around with the issue and a simple solution in this sandbox.)
Solution
In your case the simplest solution is to update the PanResponder function with the new foo
you got from useState. In your example this would look like this:
export default function App() {
const [foo, setFoo] = React.useState(0);
const panResponder = React.useRef(
PanResponder.create({
onStartShouldSetPanResponder: (evt, gestureState) => true,
onStartShouldSetPanResponderCapture: (evt, gestureState) => true,
onMoveShouldSetPanResponder: (evt, gestureState) => true,
onMoveShouldSetPanResponderCapture: (evt, gestureState) => true,
onPanResponderGrant: (evt, gestureState) => {},
onPanResponderMove: (evt, gestureState) => {
console.log(foo);
},
onPanResponderTerminationRequest: (evt, gestureState) => true,
onPanResponderRelease: (evt, gestureState) => {},
onPanResponderTerminate: (evt, gestureState) => {},
onShouldBlockNativeResponder: (evt, gestureState) => {
return true;
},
})
).current;
// update the onPanResponderMove with the new foo
panResponder.onPanResponderMove = (evt, gestureState) => {
console.log(foo);
},
return (
<View style={{ paddingTop: 200 }}>
<TouchableOpacity onPress={() => setFoo(foo + 1)}>
<Text>Foo = {foo}</Text>
</TouchableOpacity>
<View
{...panResponder.panHandlers}
style={{ marginTop: 200, backgroundColor: "grey", padding: 100 }}
>
<Text>Text for pan responder</Text>
</View>
</View>
);
}
Note
Always be extra careful if something mutable depends on your components state. If this is really necessary it is often a good idea to write a proper class or object with getters and setters. For example something like this:
const createPanResponder = (foo) => {
let _foo = foo;
const setFoo = foo => _foo = foo;
const getFoo = () => _foo;
return {
getFoo,
setFoo,
onPanResponderMove: (evt, gestureState) => {
console.log(getFoo());
},
...allYourOtherFunctions
}
}
const App = () => {
const [foo, setFoo] = React.useState(0);
const panResponder = useRef(createPanResponder(foo)).current;
panResponder.setFoo(foo);
return ( ... )
}