React Native - Device back button handling

Viewed 134694

I want to check if there are more than one screens are on stack when device back button is hit. If yes, I want to show previous screen and if no, I want to exit app.

I have checked number of examples but those use BackAndroid and Navigator. But both of them are deprecated. BackHandler is replacement for BackAndroid. And I can show previous screen by using props.navigation.goBack(null).

But I am unable to find code for finding screen count in stack. I don't want to use deprecated Navigator!

13 Answers

In functional component:

import { BackHandler } from "react-native";

function handleBackButtonClick() {
  navigation.goBack();
  return true;
}

useEffect(() => {
  BackHandler.addEventListener("hardwareBackPress", handleBackButtonClick);
  return () => {
    BackHandler.removeEventListener("hardwareBackPress", handleBackButtonClick);
  };
}, []);
 import { BackHandler } from 'react-native';
  
 constructor() {
        super();           
        this.handleBackButtonClick = this.handleBackButtonClick.bind(this);
 }

   componentWillMount() {
       BackHandler.addEventListener('hardwareBackPress', this.handleBackButtonClick);
   }

   componentWillUnmount() {
       BackHandler.removeEventListener('hardwareBackPress', this.handleBackButtonClick);
   }

   handleBackButtonClick() {
       //this.props.navigation.goBack(null);
       BackHandler.exitApp();
       return true;
   }

   handleBackButtonClick() {
       return true;   // when back button don't need to go back 
   }

In Functional Component

import { BackHandler } from 'react-native';

function handleBackButtonClick() {
    navigation.goBack();
    return true;
  }

  useEffect(() => {
    BackHandler.addEventListener('hardwareBackPress', handleBackButtonClick);
    return () => {
      BackHandler.removeEventListener('hardwareBackPress', handleBackButtonClick);
    };
  }, []);

Here is how I implemented successfully using certain condition:

componentWillMount() {
    BackHandler.addEventListener(
      'hardwareBackPress',
      this.handleBackButtonClick,
    );
  }

  componentWillUnmount() {
    BackHandler.removeEventListener(
      'hardwareBackPress',
      this.handleBackButtonClick,
    );
  }

  handleBackButtonClick = () => {
    //some condition
    if (this.state.isSearchBarActive) {
      this.setState({
        isSearchBarActive: false,
      });
      this.props.navigation.goBack(null);
      return true;
    }
    return false;
  };

try this react navigation

componentDidMount() {
        BackHandler.addEventListener('hardwareBackPress', this.handleBackButton);
    }


    handleBackButton = () => {

        const pushAction = StackActions.push({
            routeName: 'DefaultSelections',
        });

        this.props.navigation.dispatch(pushAction);
    }

current screen is "DefaultSelections" , on back button press, would be shifted on to the same and hence back button disabled work around, as disabling back button by

return true

for backButton ( as suggested by the official docs ) disables back button on all screens ; not wanted

React Native Hooks has a nice useBackHandler hook which simplifies the process of setting up event listeners for Android back button.

import { useBackHandler } from '@react-native-community/hooks'

useBackHandler(() => {
  if (shouldBeHandledHere) {
    // handle it
    return true
  }
  // let the default thing happen
  return false
})

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 /> }
}

an utility function could be very helpful:

backPressHandler.js

import React from 'react';
import {BackHandler} from 'react-native';
const onBackPress = (callback) => {
  BackHandler.addEventListener('hardwareBackPress', callback);
  return () => {
    BackHandler.removeEventListener('hardwareBackPress', callback);
  };
};

export {onBackPress};

now in my screen:

myScreen.js

import {onBackPress} from '../utils/backPressHandler';

  function handleBackPress() {
    navigation.goBack();
    return true;
  }
  useEffect(() => {
    onBackPress(handleBackPress);
  }, []);

use like in functional components

import { BackHandler } from 'react-native'; **---imports like here**

  **--click function call like below--**
 handleBackButtonClick = () => {
    navigation.goBack();
    return true;
  }

  useEffect(() => {
    BackHandler.addEventListener('hardwareBackPress', handleBackButtonClick);
    return () => {
      BackHandler.removeEventListener('hardwareBackPress', handleBackButtonClick);
    };
  }, []);

that's all. enjoy your coding...

If you use react-navigation, the other answers did not work for me but this did:

  const handleGoBack = useCallback(() => {
    // custom logic here
    return true; // Returning true from onBackPress denotes that we have handled the event
  }, [navigation]);

  useFocusEffect(
    React.useCallback(() => {
      BackHandler.addEventListener('hardwareBackPress', handleGoBack);

      return () =>
        BackHandler.removeEventListener('hardwareBackPress', handleGoBack);
    }, [handleGoBack]),

Here is the link to the documentation

I used flux for navigation.

    const RouterComp = () => {

    let backLoginScene=false;

    return (

        <Router
        backAndroidHandler={() => {
            const back_button_prohibited = ['login','userInfo','dashboard'];
            if (back_button_prohibited.includes(Actions.currentScene) ) {
                if (backLoginScene == false) {
                    ToastAndroid.show("Click back again to exit.", ToastAndroid.SHORT);
                    backLoginScene = !backLoginScene;
                    setTimeout(() => {
                        backLoginScene = false;
                    }, 2000);
                    return true;
                } else {
                    backLoginScene = false;
                    BackHandler.exitApp();
                }
                return false;
            }}}>
            <Scene key='root' hideNavBar>
                <Scene key='guest' hideNavBar >
                    <Scene key='login' component={Login} ></Scene>
                    <Scene key='userInfo' component={UserInfo}></Scene>
                </Scene>

                <Scene key='user' hideNavBar>
                    <Scene key='dashboard' component={Dashboard} title='Dashboard' initial />
                    <Scene key='newAd' component={NewAd} title='New Ad' />

                </Scene>



            </Scene>
        </Router>
    )
}

export default RouterComp;
Related