BackHandler.addEventListener('hardwareBackPress', handler) lets JavaScript answer the system Back gesture or button. Handlers run last-registered first; return true to consume the event, false to pass it on until the default finishes the activity. BookNest's catalog closes the drawer first, then asks for a second press:
const sub = BackHandler.addEventListener('hardwareBackPress', () => {
if (open.current) { // 1. back closes the drawer first
drawer.current?.closeDrawer();
console.log('back: closed the drawer');
return true;
}
if (Date.now() - lastBack.current > 2000) { // 2. first press on the catalog warns
lastBack.current = Date.now();
ToastAndroid.show('Press back again to leave BookNest', ToastAndroid.SHORT);
console.log('back: toast shown');
return true;
}
console.log('back: second press, leaving'); // 3. second press within 2 s exits
return false; // let the system finish the activity
});The handler, registered once in a useEffect that returns sub.remove(), reads refs rather than state. adb shell input keyevent KEYCODE_BACK with the drawer open, then two presses half a second apart, logged:
LOG back: closed the drawer LOG drawer closed ... LOG back: toast shown LOG back: second press, leaving
The cut lines are an earlier try with presses over 2 s apart. For apps targeting API 36, Android 16 turns on predictive back and stops calling onBackPressed(); React Native 0.86 36,878 's ReactActivity registers an OnBackPressedCallback so BackHandler still works, and BookNest's "predictiveBackGestureEnabled": false in app.json sets the temporary opt-out. While a Modal is open, use its onRequestClose instead.