17

How to exit application with twice clicking the back button without needing Redux

I was looking for a solution to limit the user and do not get out of the application with one click in react native.

Community
  • 1
  • 1
Mahdi Bashirpour
  • 17,147
  • 12
  • 117
  • 144

11 Answers11

23
import React, {Component} from 'react';
import {BackHandler, View, Dimensions, Animated, TouchableOpacity, Text} from 'react-native';

let {width, height} = Dimensions.get('window');


export default class App extends Component<Props> {

    state = {
        backClickCount: 0
    };
    
    constructor(props) {
        super(props);

        this.springValue = new Animated.Value(100) ;

    }

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

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

    _spring() {
        this.setState({backClickCount: 1}, () => {
            Animated.sequence([
                Animated.spring(
                    this.springValue,
                    {
                        toValue: -.15 * height,
                        friction: 5,
                        duration: 300,
                        useNativeDriver: true,
                    }
                ),
                Animated.timing(
                    this.springValue,
                    {
                        toValue: 100,
                        duration: 300,
                        useNativeDriver: true,
                    }
                ),

            ]).start(() => {
                this.setState({backClickCount: 0});
            });
        });

    }


    handleBackButton = () => {
        this.state.backClickCount == 1 ? BackHandler.exitApp() : this._spring();

        return true;
    };


    render() {

        return (
            <View style={styles.container}>
                <Text>
                    container box
                </Text>

                <Animated.View style={[styles.animatedView, {transform: [{translateY: this.springValue}]}]}>
                    <Text style={styles.exitTitleText}>press back again to exit the app</Text>

                    <TouchableOpacity
                        activeOpacity={0.9}
                        onPress={() => BackHandler.exitApp()}
                    >
                        <Text style={styles.exitText}>Exit</Text>
                    </TouchableOpacity>

                </Animated.View>
            </View>
        );
    }
}


const styles = {
    container: {
        flex: 1,
        justifyContent: "center",
        alignItems: "center"
    },
    animatedView: {
        width,
        backgroundColor: "#0a5386",
        elevation: 2,
        position: "absolute",
        bottom: 0,
        padding: 10,
        justifyContent: "center",
        alignItems: "center",
        flexDirection: "row",
    },
    exitTitleText: {
        textAlign: "center",
        color: "#ffffff",
        marginRight: 10,
    },
    exitText: {
        color: "#e5933a",
        paddingHorizontal: 10,
        paddingVertical: 3
    }
};

Run in snack.expo: https://snack.expo.io/HyhD657d7

Mahdi Bashirpour
  • 17,147
  • 12
  • 117
  • 144
  • Can you check this, Please Mr. Mahdi! https://stackoverflow.com/questions/55306718/how-to-handle-exit-app-in-one-screen-not-in-the-whole-app-react-native – DevAS Mar 22 '19 at 21:32
  • I dont think we require such complex examples.Check out my answer. – manish kumar Mar 28 '20 at 17:57
  • @manishkumar Thankful. This answer is a year and a half ago and needs to be reviewed. I also offered you a concession – Mahdi Bashirpour Mar 28 '20 at 20:40
  • you can update the answer based on recent changes. Since the question isnt version based. or mention the version. Would be helpful to the users. – manish kumar Mar 29 '20 at 06:30
  • 1
    Added UNSAFE_componentWillMount & UNSAFE_componentWillUnmount now working without any warnings – subramanya46 Jun 23 '20 at 07:08
13

I've solved it this way as separated functional Component. This way you don't need to recode it for each app, only include the component in your new App and you've done!

import * as React from 'react';
import {useEffect, useState} from 'react';
import {Platform, BackHandler, ToastAndroid} from 'react-native';

export const ExecuteOnlyOnAndroid = (props) => {
  const {message} = props;
  const [exitApp, setExitApp] = useState(0);
  const backAction = () => {
    setTimeout(() => {
      setExitApp(0);
    }, 2000); // 2 seconds to tap second-time

    if (exitApp === 0) {
      setExitApp(exitApp + 1);

      ToastAndroid.show(message, ToastAndroid.SHORT);
    } else if (exitApp === 1) {
      BackHandler.exitApp();
    }
    return true;
  };
  useEffect(() => {
    const backHandler = BackHandler.addEventListener(
      'hardwareBackPress',
      backAction,
    );
    return () => backHandler.remove();
  });
  return <></>;
};

export default function DoubleTapToClose(props) {
  const {message = 'tap back again to exit the App'} = props;
  return Platform.OS !== 'ios' ? (
    <ExecuteOnlyOnAndroid message={message} />
  ) : (
    <></>
  );
}

=> Only thing you need is to include this component in your App. <=

Because IOS don't have a back-button, IOS don't need this functionality. The above Component automatically detect if Device is Android or not.

By default the Message shown in Toast is predefined in English, but you can set your own one if you add an property named message to your DoubleTapToClose-Component.

...
import DoubleTapToClose from '../lib/android_doubleTapToClose'; 
...
return(
    <>
        <DoubleTapToClose />
        ...other Stuff goes here
    </>

Depending on your Nav-Structure, you have to check if you are at an initialScreen of your App or not. In my case, I have an Drawer with multiples StackNavigatiors inside. So I check if the current Screen is an initial-Screen (index:0), or not. If it is, I set an Hook-Variable which will only be used for those initial-Screens.

Looks like this:

const isCurrentScreenInitialOne = (state) => {
  const route = state.routes[state.index];
  if (route.state) {
    // Dive into nested navigators
    return isCurrentScreenInitialOne(route.state);
  }
  return state.index === 0;
};

...
...

export default function App() {
...
  const [isInitialScreen, setIsInitialScreen] = useState(true);

  {isInitialScreen && (<DoubleTapToClose message="Tap again to exit app" />)}

...
...
<NavigationContainer
          ...
          onStateChange={(state) => {
            setIsInitialScreen(isCurrentScreenInitialOne(state));
          }}>

If that description helps you out, don't miss to vote.

suther
  • 12,600
  • 4
  • 62
  • 99
  • 3
    What a clever approach @suther! – LordKiz May 18 '20 at 13:54
  • instead of all the checkings, doesn't `canGoBack` just tell you whether you are about to exit the app? – Exis Zhang Jul 14 '20 at 00:38
  • @exis-zhang: There is no `canGoBack` Function in `React-Navigation`. It seems you mixed-up `reactJS` (and Browser-API... there you have an canGoBack) and `react-native` / `react-navigation`. – suther Jul 14 '20 at 10:28
6

The most simple way (paste directly in your functional component):

const navigation = useNavigation();
const navIndex = useNavigationState(s => s.index);
const [backPressCount, setBackPressCount] = useState(0);

const handleBackPress = useCallback(() => {
  if (backPressCount === 0) {
    setBackPressCount(prevCount => prevCount + 1);
    setTimeout(() => setBackPressCount(0), 2000);
    ToastAndroid.show('Press one more time to exit', ToastAndroid.SHORT);
  } else if (backPressCount === 1) {
    BackHandler.exitApp();
  }
  return true;
}, [backPressCount]);

useEffect(() => {
  if (Platform.OS === 'android' && navIndex === 0) {
    const backListener = BackHandler.addEventListener(
      'hardwareBackPress',
      handleBackPress,
    );
    return backListener.remove;
  }
}, [handleBackPress]);

You can also wrap it in your custom hook and use the hook in the component(s).

Anton Liannoi
  • 543
  • 5
  • 13
5

Sorry if I'm late to the party, but I had a similar requirement and solved it by creating my own custom hook!

let currentCount = 0;
export const useDoubleBackPressExit = (exitHandler: () => void) => {
  if (Platform.OS === "ios") return;
  const subscription = BackHandler.addEventListener("hardwareBackPress", () => {
    if (currentCount === 1) {
      exitHandler();
      subscription.remove();
      return true;
    }
    backPressHandler();
    return true;
  });
};
const backPressHandler = () => {
  if (currentCount < 1) {
    currentCount += 1;
    WToast.show({
      data: "Press again to close!",
      duration: WToast.duration.SHORT,
    });
  }
  setTimeout(() => {
    currentCount = 0;
  }, 2000);
};

Now I can use it anywhere I want by simply doing:

useDoubleBackPressExit(() => { 
   // user has pressed "back" twice. Do whatever you want! 
});
Bhargav Rao
  • 50,140
  • 28
  • 121
  • 140
Ayusch
  • 417
  • 7
  • 17
3

A Better Approach would be simply to use BackHandler and ToastAndroid

import { BackHandler, ToastAndroid } from 'react-native';
//rest of imports

class SomeClass extends Component{
    constructor(state, props) {
        super(state, props)
        this.state = {
            validCloseWindow: false
        }
    }
    async componentDidMount() {
        BackHandler.addEventListener('hardwareBackPress', this.handleBackButton.bind(this));
    }

    componentWillUnmount() {
        BackHandler.removeEventListener('hardwareBackPress', this.handleBackButton.bind(this));
    }
    handleBackButton = () => {
        if (!this.props.navigation.canGoBack()) {
            if (this.state.validCloseWindow)
                return false;
            this.state.validCloseWindow = true
            setTimeout(() => {
                this.state.validCloseWindow = false
            }, 3000);
            ToastAndroid.show("Press Again To Exit !", ToastAndroid.SHORT);
            return true;
        }
    };
//rest of component code
}

Just make sure to use it on the initialRoute page for your navigation.

manish kumar
  • 4,412
  • 4
  • 34
  • 51
2

Below Code Explains itself. The trick is to have it in the Main AppContainer rather in every page

import {  Alert,  BackHandler,  ToastAndroid } from 'react-native';
import {  StackActions } from 'react-navigation';
// common statless class variable.
let backHandlerClickCount = 0;

class App extends React.Component {
    constructor(props) {
      super(props);
      // add listener to didFocus
      this._didFocusSubscription = props.navigation.addListener('didFocus', payload =>
        BackHandler.addEventListener('hardwareBackPress', () => this.onBackButtonPressAndroid(payload)));
    }

    // remove listener on unmount 
    componentWillUnmount() {
      if (this._didFocusSubscription) {
        this._didFocusSubscription.remove();
      }
    }

    onBackButtonPressAndroid = () => {
      const shortToast = message => {
        ToastAndroid.showWithGravityAndOffset(
          message,
          ToastAndroid.SHORT,
          ToastAndroid.BOTTOM,
          25,
          50
        );

        const {
          clickedPosition
        } = this.state;
        backHandlerClickCount += 1;
        if ((clickedPosition !== 1)) {
          if ((backHandlerClickCount < 2)) {
            shortToast('Press again to quit the application!');
          } else {
            BackHandler.exitApp();
          }
        }

        // timeout for fade and exit
        setTimeout(() => {
          backHandlerClickCount = 0;
        }, 2000);

        if (((clickedPosition === 1) &&
            (this.props.navigation.isFocused()))) {
          Alert.alert(
            'Exit Application',
            'Do you want to quit application?', [{
              text: 'Cancel',
              onPress: () => console.log('Cancel Pressed'),
              style: 'cancel'
            }, {
              text: 'OK',
              onPress: () => BackHandler.exitApp()
            }], {
              cancelable: false
            }
          );
        } else {
          this.props.navigation.dispatch(StackActions.pop({
            n: 1
          }));
        }
        return true;
      }

    }
Mukundhan
  • 3,284
  • 23
  • 36
  • 1
    can you check this, please !! https://stackoverflow.com/questions/55306718/how-to-handle-exit-app-in-one-screen-not-in-the-whole-app-react-native – DevAS Mar 22 '19 at 21:33
2

Simplest One

import { useNavigationState } from '@react-navigation/native';
import { BackHandler, Alert } from 'react-native';
//index to get index of home screen when index == 0 in navigation stack
const index = useNavigationState(state => state.index);

const backAction = () => {
    Alert.alert("Hold on!", "Are you sure you want to Exit App?", [
        {
            text: "Cancel",
            onPress: () => null,
            style: "cancel"
        },
        { text: "YES", onPress: () => BackHandler.exitApp() }
    ]);
    return true;
};

useEffect(() => {
    // if index==0 this is initial screen 'Home Screen'
    if (index == 0) {
        BackHandler.addEventListener("hardwareBackPress", backAction);
        return () =>
            BackHandler.removeEventListener("hardwareBackPress", backAction);
    }
}, [index]);
مصطفى
  • 555
  • 4
  • 9
1
import React, { Component } from 'react'
import { Text, View, StyleSheet, TouchableOpacity,BackHandler} from 'react-native'
import { Toast } from "native-base";

class Dashboard extends Component {

    state={
        clickcount:0
    }


    componentDidMount(){
        BackHandler.addEventListener("hardwareBackPress",()=>{
            this.setState({'clickcount':this.state.clickcount+1})
            this.check();
            return true
        })
    }

    check=()=>{
        if(this.state.clickcount<2){
            Toast.show({
                text:`Press back again to exit App `,
                duration:2000,
                onClose:()=>{this.setState({'clickcount':0})}
            })

        }
        else if(this.state.clickcount==2)
        {
            BackHandler.exitApp()
        }
    }

    render() {
        return (

                <View style={styles.container}>
                    <Text> Hello this is the Dashboard Screen</Text>
                </View>

        )
    }
}

export default Dashboard

const styles = StyleSheet.create({
 container:{
     flex:1,
     marginTop:25,
     justifyContent:'center',
     alignItems:'center',
     borderBottomLeftRadius:30,
     borderBottomRightRadius:30,
     backgroundColor:'#fff'
    },
  });
Reza Ghorbani
  • 2,396
  • 2
  • 28
  • 33
1

the most simple approach for now:

in App.js:

componentDidMount() {
    const backHandler=BackHandler.addEventListener('hardwareBackPress', ()=>{
        if(this.backHandler){
            return false;
        }
        Toast.show('再按一次退出应用');
        this.backHandler=backHandler;
        setTimeout(()=>{
            this.backHandler=null;
        },2000);
        return true;
    });
}

componentWillUnmount() {
    this.backHandler.remove();
}
Tonyxiang
  • 11
  • 1
1

The simplest solution I used in my app is this. It works fine with react- navigation 4.4.1 and is much shorter than some other correct answers provided here.

import React from 'react';
import { BackHandler, ToastAndroid} from 'react-native';

export default class LoginScreen extends React.Component {

    state = {
        canBeClosed: false
    }

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

    handleBackButton = () => {
        if (this.props.navigation.isFocused()) {
            if (this.state.canBeClosed)
                return this.state.canBeClosed = false;
    
            else {
                setTimeout(() => { this.state.canBeClosed = false }, 3000);    
                ToastAndroid.show("Press Again To Exit !", ToastAndroid.SHORT);
    
                return this.state.canBeClosed = true
            }   
        }
    };

 //some code

}
RegRom
  • 128
  • 10
0

here is my solution, I don't used state, I have one main navigationContainer with nested navigator.

Inside main navigation:

imports...

const Stack = createNativeStackNavigator();

// this variable is never recreated
let exitCount = 0;

const Navigation = () => {

  // action triggered on android back btn 
  const androidBackAction = () => {
    // here is a little problem because I never clear the timeout, so you could have multiple timeout subscription at the same time.
    // nonetheless after 2sec they clear by themselves, I think :)
    setTimeout(() => {
      exitCount = 0;
    }, 2000);

    if (exitCount === 0) {
      ToastAndroid.showWithGravity(
        "Click two times to close!",
        ToastAndroid.SHORT,
        ToastAndroid.BOTTOM
      );
      exitCount += 1;
    } else if (exitCount === 1) {
      BackHandler.exitApp();
    }
    return true;
  };

  const navigationChangeHandler = (state: NavigationState | undefined) => {
    if (Platform.OS === "ios") return;
    // check if current route is the initial one of the main navigation
    // get the navigation
    const currentNav = state?.routes[state.index];
    // get inner route index
    const nestedNavIndex = currentNav?.state?.index;

    // back handler to exit the app
    const androidBackExitHandler = BackHandler.addEventListener(
      "hardwareBackPress",
      androidBackAction
    );
    // remove exit app callback if inside not first route of nested nav
    // => restore normal back behaviour
    if (nestedNavIndex !== 0) {
      androidBackExitHandler.remove();
    }
  };

  return (
    <NavigationContainer onStateChange={navigationChangeHandler}>
      <Stack.Navigator
        initialRouteName={NavConst.LOADING}
        screenOptions={{
          headerShown: false,
          contentStyle: {
            backgroundColor: Color.accent,
          },
          animation: "none",
        }}
      >
        <Stack.Screen name={NavConst.LOADING} component={WaitingScreen} />
        <Stack.Screen name={NavConst.TUTORIAL} component={Tutorial} />
        <Stack.Screen name={NavConst.LOGIN_NAV} component={LoginNavigator} />
        <Stack.Screen name={NavConst.MAIN_NAV} component={MainNavigator} />
      </Stack.Navigator>
    </NavigationContainer>
  );
};

export default Navigation;

I hope it could help!

R. Milos
  • 43
  • 1
  • 7