I am on v0.46.0 of react-native and had the same issue. I tracked the issue down to this file in the react-native code base
https://github.com/facebook/react-native/blob/master/Libraries/Utilities/BackHandler.android.js#L25
When running with the chrome debugger turned off the line
var subscriptions = Array.from(_backPressSubscriptions.values()).reverse()
always returns an empty array for subscriptions which in turn causes the invokeDefault variable to stay true and the .exitApp() function to be called.
After more investigation, I think the issue was discovered and discussed in the following PR #15182.
Even after copy/pasting the PR change in an older version of RN it did not work most likely caused by the issue described in the PR.
After some very slight modifications I got it working by changing to
RCTDeviceEventEmitter.addListener(DEVICE_BACK_EVENT, function() {
var invokeDefault = true;
var subscriptions = []
_backPressSubscriptions.forEach(sub => subscriptions.push(sub))
for (var i = 0; i < subscriptions.reverse().length; ++i) {
if (subscriptions[i]()) {
invokeDefault = false;
break;
}
}
if (invokeDefault) {
BackHandler.exitApp();
}
});
Simply using a .forEach which was the original implementation on the PR before the amended Array.from syntax works throughout.
So you could fork react-native and use a modified version, submit a PR though I imagine that will take a little while to be approved and merged upstream, or you can do something similar to what I did which was to override the RCTDeviceEventEmitter.addListener(...) for the hardwareBackPress event.
// other imports
import { BackHandler, DeviceEventEmitter } from 'react-native'
class MyApp extends Component {
constructor(props) {
super(props)
this.backPressSubscriptions = new Set()
}
componentDidMount = () => {
DeviceEventEmitter.removeAllListeners('hardwareBackPress')
DeviceEventEmitter.addListener('hardwareBackPress', () => {
let invokeDefault = true
const subscriptions = []
this.backPressSubscriptions.forEach(sub => subscriptions.push(sub))
for (let i = 0; i < subscriptions.reverse().length; i += 1) {
if (subscriptions[i]()) {
invokeDefault = false
break
}
}
if (invokeDefault) {
BackHandler.exitApp()
}
})
this.backPressSubscriptions.add(this.handleHardwareBack)
}
componentWillUnmount = () => {
DeviceEventEmitter.removeAllListeners('hardwareBackPress')
this.backPressSubscriptions.clear()
}
handleHardwareBack = () => { /* do your thing */ }
render() { return <YourApp /> }
}