React Native's panResponder has stale value from useState?
Asked Answered
P

4

15

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>
  );
}
Procurator answered 3/4, 2020 at 14:30 Comment(0)
P
1

With useEffect you can create a new PanResponder when foo changes. However I'm not sure how performant this is.

export default function App() {
  const [foo, setFoo] = React.useState(0);

  const pan = 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;
    },
  });

  const [panResponder, setPanResponder] = React.useState(pan);

  useEffect(() => {
    setPanResponder(pan);
  }, [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>
  );
}
Procurator answered 6/4, 2020 at 13:31 Comment(0)
M
23

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
  );
Mightily answered 11/4, 2020 at 8:51 Comment(1)
Finally I understand when to use useMemo. Thank youHachman
C
3

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

}

Cohbath answered 6/4, 2020 at 18:48 Comment(0)
T
2

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

Telamon answered 6/4, 2020 at 13:24 Comment(6)
I added a Snack example. Maybe that helps clarify a bit.Telamon
You're doing something different in that Snack. You're not accessing foo in onPanResponderMoveProcurator
I update the Snack example along with answer @ProcuratorTelamon
Does that Snack work? It doenst seem to drag or log.Procurator
It doesn't drag, I removed that functionality. But it should log perfectly. Try dragging it and you'll see it responds (won't move but it'll respond).Telamon
Now with dragging and logging capabilities. Here's a gist of the entire app file.Telamon
P
1

With useEffect you can create a new PanResponder when foo changes. However I'm not sure how performant this is.

export default function App() {
  const [foo, setFoo] = React.useState(0);

  const pan = 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;
    },
  });

  const [panResponder, setPanResponder] = React.useState(pan);

  useEffect(() => {
    setPanResponder(pan);
  }, [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>
  );
}
Procurator answered 6/4, 2020 at 13:31 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.