Merouane
Merouane

Reputation: 43

React Native BackHandler Not Following State Updates

My backAction function that's used in BackHandler event listener, uses a stage state for conditionals, but doesn't follow the state value updates when stage is updated by other components, as it gets stuck on the initial state value. and also return true and false don't do anything.

Expected Behavior:

If back button is pressed check current stage:

Current Behavior:

Once back button is pressed:

Code To Reproduce:

const [stage, setStage] = useState(1);

const backAction = async () => {
  console.log(stage); // the bug: stage is always 1

  if (stage === 1) {
    console.log("Going back to previous screen!");
    return false;
  } else if (stage > 1) {
    console.log("Setting stage back with -1");
    await setStage((prevState) => prevState - 1);
    return true;
  }

  return false;
};

useEffect(() => {
  BackHandler.addEventListener("hardwareBackPress", backAction);

  return () => BackHandler.removeEventListener("hardwareBackPress", backAction);
}, []);

Note: I've used the documentation pattern of assigning BackHandler.addEventListener to backHandler, and then using it in the events clean up as backHandler.remove(). didn't work either, but removed it here for the sake of simplicity.

Any help would be much appreciated.

Upvotes: 3

Views: 2531

Answers (1)

Tom
Tom

Reputation: 9127

First: the return value of your event handler is passed to the JS runtime's event listener system. It doesn't accept promises, it doesn't know how to accept promises. But a promise is a truthy value, which means that your handler is effectively returning true simply because you marked it async.

Second, setState is not asynchronous (i.e. is not declared async), which means there's no need to await it. The code will still run, but there's no benefit. And, since this is the only use of async in the handler, removing this allows you to convert your handler to synchronous code, which is a necessity for responding to the hardwareBackPress correctly.

Third, returning false is the opposite of what you should do if you want the native behavior to occur. You must return true to get default behavior, and false otherwise.

The above paragraph is false. Per this documentation, you had the right idea about return values. (My apologies; my react-native skills are rusty. I've corrected the code sample below to reflect this.)

Third (for realz): you need to tell React Hooks to re-bind the event handler whenever the value of stage changes. The reason this is needed is that the closure will preserve the value of stage from when the function is defined, and it doesn't receive updates. You do this by listing that variable among the hook's dependencies. This is why you're seeing stale values at runtime.

Try something like this instead:

const [stage, setStage] = useState(1);

const backAction = () => {
  console.log(stage);

  if (stage === 1) {
    console.log("Going back to previous screen!");
    return false;
  } else if (stage > 1) {
    console.log("Setting stage back with -1");
    setStage((prevState) => prevState - 1);
    return true;
  }

  return false;
};

useEffect(() => {
  BackHandler.addEventListener("hardwareBackPress", backAction);

  return () => BackHandler.removeEventListener("hardwareBackPress", backAction);
}, [ stage ]);  // <-- this is the part you're missing

Upvotes: 13

Related Questions