Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

React Native's panResponder has stale value from useState?

Tags:

react-native

I need to read the value of useState in onPanResponderMove. On page load onPanResponderMove correctly logs the initial value of 0.

However after I click on TouchableOpacity to increment foo, the onPanResponderMove stills logs out 0 rather than it's new value.

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); // This is always 0
      },
      onPanResponderTerminationRequest: (evt, gestureState) => true,
      onPanResponderRelease: (evt, gestureState) => {},
      onPanResponderTerminate: (evt, gestureState) => {},
      onShouldBlockNativeResponder: (evt, gestureState) => {
        return true;
      },
    })
  ).current;

  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>
  );
}
like image 669
Evanss Avatar asked Apr 03 '20 14:04

Evanss


Video Answer


3 Answers

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
  );
like image 130
LonelyCpp Avatar answered Oct 12 '22 14:10

LonelyCpp


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

}

like image 3
Leo Avatar answered Oct 12 '22 14:10

Leo


It looks like you're passing foo in an attempt to update the existing state. Instead, pass in the previous state and update it accordingly (functional update). Like this:

<TouchableOpacity onPress={() => setFoo(f =>  f + 1)}>
  <Text>Foo = {foo}</Text>
</TouchableOpacity>

To make the current value of foo available inside the onPanResponderMove handler. Create a ref.

According to the docs:

The “ref” object is a generic container whose current property is mutable and can hold any value

So, with that in mind we could write:

const [foo, setFoo] = React.useState(0);

const fooRef = React.useRef()
React.useEffect(() => {
  fooRef.current = foo
},[foo])

Then inside the onPanResponderMove handler you can access the ref's current value that we set before:

onPanResponderMove: (evt, gestureState) => {
  console.log('fooRef', fooRef.current)
  alert(JSON.stringify(fooRef))
},

Working example here

More info on stale state handling here

like image 1
Juan Marco Avatar answered Oct 12 '22 13:10

Juan Marco