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 ( ... )

}

Tags:

React Native