Reputation: 43
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:
return false
to trigger back button default behavior and navigate back to the previous screen (with out the need to use navigation.goBack()
).return true
to stop the event bubbling.Current Behavior:
Once back button is pressed:
stage
initial value of 1 even if it's clear that other components changed it and used it's new value.return false
doesn't even trigger the default behavior of the back button.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
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