React Native: Double back press to Exit App
Asked Answered
P

12

19

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.

Pothouse answered 10/9, 2018 at 7:34 Comment(2)
Ch Soal khubi ... Damet garm javab awli vase appam gereftamLias
tashakor baradar azizam @MoHammaDReZaDehGhaniPothouse
P
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

Pothouse answered 10/9, 2018 at 7:34 Comment(5)
Can you check this, Please Mr. Mahdi! #55307218Adolphadolphe
I dont think we require such complex examples.Check out my answer.Hemihedral
@manishkumar Thankful. This answer is a year and a half ago and needs to be reviewed. I also offered you a concession 😊Pothouse
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.Hemihedral
Added UNSAFE_componentWillMount & UNSAFE_componentWillUnmount now working without any warningsRevers
G
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.

Genesisgenet answered 4/5, 2020 at 17:57 Comment(3)
What a clever approach @suther!Beaulahbeaulieu
instead of all the checkings, doesn't canGoBack just tell you whether you are about to exit the app?Jehovah
@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.Genesisgenet
M
9

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).

Manhattan answered 27/4, 2022 at 17:3 Comment(0)
B
6

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! 
});
Barvick answered 4/8, 2020 at 16:10 Comment(0)
H
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.

Hemihedral answered 28/3, 2020 at 17:55 Comment(0)
H
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;
      }

    }
Heinous answered 20/11, 2018 at 9:48 Comment(1)
can you check this, please !! #55307218Adolphadolphe
Z
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]);
Zoology answered 6/3, 2022 at 6:50 Comment(2)
Where would you put this in the NavigationContainer? Create a wrapper for each screen?Perianth
in Home Screen that you want to show this alertPali
S
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'
    },
  });
Selfwinding answered 13/2, 2020 at 15:39 Comment(2)
Welcome to SO! Please consider formatting your code. Thanks!Shondrashone
Use ToastAndroid from react-native library instead. Check out my answerHemihedral
O
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();
}
Ownership answered 3/6, 2020 at 5:30 Comment(0)
M
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

}
Moulin answered 14/11, 2020 at 17:38 Comment(0)
C
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!

Crapulous answered 2/11, 2022 at 10:15 Comment(0)
B
0

For expo-router, all of the above didn't directly work with me. I finally made my own below using "@Anton Liannoi" answer.

My requirement: It should work only on the first screen of app.

Use the hook in the root _layout.tsx.

Hook code below 👇:

import { router } from "expo-router";
import { useCallback, useEffect, useState } from "react";
import { BackHandler, Platform, ToastAndroid } from "react-native";
    
export const useDoubleBackPressExit = () => {
  const [backPressCount, setBackPressCount] = useState(0);
    
  const handleBackPress = useCallback(() => {
    if (!router.canGoBack()) {
      if (backPressCount === 0) {
        setBackPressCount(prevCount => prevCount + 1);
        setTimeout(() => setBackPressCount(0), 2000);
        ToastAndroid.show('Press again to exit', ToastAndroid.SHORT);
      } else if (backPressCount === 1) {
        BackHandler.exitApp();
      }
    } else {
      router.back();
    }
    return true;
  }, [backPressCount]);
    
  useEffect(() => {
    if (Platform.OS === 'android') {
      const backListener = BackHandler.addEventListener(
       'hardwareBackPress',
        handleBackPress,
      );
      return backListener?.remove; 
    }
  }, [handleBackPress])
}
Bonanza answered 20/1 at 13:55 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.