72

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!

jted95
  • 1,084
  • 1
  • 9
  • 23
Virat18
  • 3,427
  • 2
  • 23
  • 29

14 Answers14

104

This example will show you back navigation which is expected generally in most of the flows. You will have to add following code to every screen depending on expected behavior. There are 2 cases: 1. If there are more than 1 screen on stack, device back button will show previous screen. 2. If there is only 1 screen on stack, device back button will exit app.

Case 1: Show previous screen

import { BackHandler } from 'react-native';

constructor(props) {
    super(props)
    this.handleBackButtonClick = this.handleBackButtonClick.bind(this);
}

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

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

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

Important: Don't forget to bind method in constructor and to remove listener in componentWillUnmount.

Case 2: Exit App

In this case, no need to handle anything on that screen where you want to exit app.

Important: This should be only screen on stack.

Graham
  • 7,431
  • 18
  • 59
  • 84
Virat18
  • 3,427
  • 2
  • 23
  • 29
  • this.props.navigation.goBack(null); shows a blank screen in my case. What may've caused it? I've a tabView nested under stackView and on my tabView when 'this.props.navigation.goBack(null)' is executed blank screen appears. – Pavan Nov 08 '17 at 10:32
  • When I am using this code, I am getting error message "Can't read property navigation of undefined", what can be the issue? – Anjana Jan 05 '18 at 06:28
  • 1
    My App is directly exiting whenever I am clicking on device back button from any screen. – Anjana Jan 05 '18 at 06:36
  • 4
    If we handle in this way we have to handle it for all the screens. Is there any way to handle it globally???? – karthik vishnu kumar Mar 06 '18 at 05:25
  • If you are using facebook `flow` checker, you should use `handleBackButtonClick = () => { ... }` to eliminate the annoying error – MewX Sep 18 '18 at 06:39
  • Doesn't works, I have 2 screen. LoginScreen -> HomeScreen. I add on HomeScreen, when I press physical button it doesn't works. What happend? – Yohanim Jan 16 '19 at 15:47
  • It's so incredibly annoying that I need to edit 4 methods and add 1 import for every screen in my app when this should be the default behavior. – George Jan 27 '19 at 16:11
  • Note that `componentWillMount` is not recommended by React devs, and it will be removed in future versions. I tested Your example with `component DID Mount` and it worked. :) Thank You for this clean solution. @Virat18 – Aleksandar Jun 26 '19 at 08:16
  • 2
    You don't have to `bind` the method if You use *arrow* function like this: `handleBackButtonClick = () => {...}` – Aleksandar Jun 26 '19 at 08:17
  • I copied the code. But not working if I use this for exiting the tab navigator screen to other stack navigator screen. Only componentWillMount function is executing not others. Please help me out. – Utkarsh Jun 10 '20 at 08:46
  • But How How do I navigate Whatever Page I want. Means When I put My screen name to Your navigation.navigate then its navigate only previous screen I want it to navigate Screen name i given to it –  Feb 05 '21 at 09:53
56

In functional component:

import { BackHandler } from "react-native";

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

useEffect(() => {
  BackHandler.addEventListener("hardwareBackPress", handleBackButtonClick);
  return () => {
    BackHandler.removeEventListener("hardwareBackPress", handleBackButtonClick);
  };
}, []);
Gandalf
  • 2,921
  • 5
  • 31
  • 44
Aurangzaib Rana
  • 4,028
  • 1
  • 14
  • 23
  • We can't use this in android as we come across this Error: `Warning: An effect function must not return anything besides a function, which is used for clean-up.` – Prithin Babu Apr 29 '22 at 14:00
12
 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);
    };
  }, []);
Keshav Gera
  • 10,807
  • 1
  • 75
  • 53
4

In a case where there are more than one screens stacked in the stack, the default back button behavior in react-native is to navigate back to the previous screen in the stack. Handling the device back button press when having only one screen to exit the app requires a custom setting. Yet this can be achieved without having to add back handling code to each and every screen by modifying the getStateForAction method of the particular StackNavigator's router.

Suppose you have the following StackNavigator used in the application

const ScreenStack = StackNavigator(
  {
    'Screen1': {
      screen: Screen1
    },
    'Screen2': {
      screen: Screen2
    },
  },
  {
    initialRouteName: 'Screen1'
  }
);

The getStateForAction method of the stack navigator's router can be modified as follows to achieve the expected back behavior.

const defaultStackGetStateForAction =
  ScreenStack.router.getStateForAction;

ScreenStack.router.getStateForAction = (action, state) => {
  if(state.index === 0 && action.type === NavigationActions.BACK){
    BackHandler.exitApp();
    return null;
  }

  return defaultStackGetStateForAction(action, state);
};

the state.index becomes 0 only when there is one screen in the stack.

truchiranga
  • 469
  • 5
  • 10
4

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;
  };
Harshal
  • 7,562
  • 2
  • 30
  • 20
3

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
})
Pavel Chuchuva
  • 22,633
  • 10
  • 99
  • 115
2

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

Shubham Kakkar
  • 581
  • 6
  • 4
2

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);
  }, []);

Rafiq
  • 8,987
  • 4
  • 35
  • 35
1

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 /> }
}
Gani Siva kumar
  • 161
  • 1
  • 10
1
import { useFocusEffect} from '@react-navigation/native';

  export default function App(props: any) {    
     function handleBackButton() {
         navigation.goBack();
        return true;
    }

  useFocusEffect(
          React.useCallback(() => {
          BackHandler.addEventListener("hardwareBackPress", handleBackButton);

    return () => {
      console.log("I am removed from stack")
      BackHandler.removeEventListener("hardwareBackPress", handleBackButton);
     };
   }, [])
 );
}
Sydney_dev
  • 1,448
  • 2
  • 17
  • 24
naveed ahmed
  • 161
  • 1
  • 6
  • As it’s currently written, your answer is unclear. Please [edit] to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Jan 24 '23 at 18:54
0
constructor(props){
    super(props)
    this.onBackPress = this.onBackPress.bind(this);
}

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

}

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

onBackPress(){
    const {dispatch, nav} = this.props;
    if (nav.index < 0) {
        return false;
    }
    dispatch(NavigationActions.back());
    return true;
}

render(){
    const {dispatch, nav} = this.props;
    return(
        <DrawerRouter
            navigation= {
                addNavigationHelpers({
                    dispatch,
                    state: nav,
                    addListener,
                })
            }
        />
    );
}
yamaha
  • 1
0

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

Kevin Amiranoff
  • 13,440
  • 11
  • 59
  • 90
0
    useFocusEffect(
React.useCallback(() => {
  const onBackPress = () => {
    navigation.navigate('Journal'); 
    return true;
  };      
  BackHandler.addEventListener('hardwareBackPress', onBackPress);
  return () => {
    BackHandler.removeEventListener('hardwareBackPress', onBackPress);
  };
}, []),

);

`

ummar
  • 1
  • 1
  • Your answer could be improved with additional supporting information. Please [edit] to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Nov 15 '22 at 04:42
-1

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;
Akın Köker
  • 405
  • 5
  • 7