Confirm/warn dialog on back - react-native

Like in the web browser, we have onBeforeUnload (vs onUnload) to show a alert or some warning "there is unsaved data - are you sure you want to go back".
I am trying to do the same. I couldn't find anything in the docs of react-navigation.
I thought of doing something real hacky like this, but I don't know if its the right way:
import React, { Component } from 'react'
import { StackNavigator } from 'react-navigation'
export default function ConfirmBackStackNavigator(routes, options) {
const StackNav = StackNavigator(routes, options);
return class ConfirmBackStackNavigatorComponent extends Component {
static router = StackNav.router;
render() {
const { state, goBack } = this.props.navigation;
const nav = {
...this.props.navigation,
goBack: () => {
showConfirmDialog()
.then(didConfirm => didConfirm && goBack(state.key))
}
};
return ( <StackNav navigation = {nav} /> );
}
}
}

React navigation 5.7 has added support for it:
function EditText({ navigation }) {
const [text, setText] = React.useState('');
const hasUnsavedChanges = Boolean(text);
React.useEffect(
() =>
navigation.addListener('beforeRemove', (e) => {
if (!hasUnsavedChanges) {
// If we don't have unsaved changes, then we don't need to do anything
return;
}
// Prevent default behavior of leaving the screen
e.preventDefault();
// Prompt the user before leaving the screen
Alert.alert(
'Discard changes?',
'You have unsaved changes. Are you sure to discard them and leave the screen?',
[
{ text: "Don't leave", style: 'cancel', onPress: () => {} },
{
text: 'Discard',
style: 'destructive',
// If the user confirmed, then we dispatch the action we blocked earlier
// This will continue the action that had triggered the removal of the screen
onPress: () => navigation.dispatch(e.data.action),
},
]
);
}),
[navigation, hasUnsavedChanges]
);
return (
<TextInput
value={text}
placeholder="Type something…"
onChangeText={setText}
/>
);
}
Doc: https://reactnavigation.org/docs/preventing-going-back

On current screen set
this.props.navigation.setParams({
needUserConfirmation: true,
});
In your Stack
const defaultGetStateForAction = Stack.router.getStateForAction;
Stack.router.getStateForAction = (action, state) => {
if (state) {
const { routes, index } = state;
const route = get(routes, index);
const needUserConfirmation = get(route.params, 'needUserConfirmation');
if (
needUserConfirmation &&
['Navigation/BACK', 'Navigation/NAVIGATE'].includes(action.type)
) {
Alert.alert('', "there is unsaved data - are you sure you want to go back", [
{
text: 'Close',
onPress: () => {},
},
{
text: 'Confirm',
onPress: () => {
delete route.params.needUserConfirmation;
state.routes.splice(index, 1, route);
NavigationService.dispatch(action);
},
},
]);
// Returning null from getStateForAction means that the action
// has been handled/blocked, but there is not a new state
return null;
}
}
return defaultGetStateForAction(action, state);
};
Notes,
Navigating without the navigation prop
https://reactnavigation.org/docs/en/navigating-without-navigation-prop.html
NavigationService.js
function dispatch(...args) {
_navigator.dispatch(...args);
}

This can be accomplished by displaying a custom back button in the header, and capturing the hardware back-event before it bubbles up to the navigator.
We'll first configure our page to show a custom back button by overriding the navigation options:
import React, { Component } from 'react'
import { Button } from 'react-native'
function showConfirmDialog (onConfirmed) { /* ... */ }
class MyPage extends Component {
static navigationOptions ({ navigation }) {
const back = <Button title='Back' onPress={() => showConfirmDialog(() => navigation.goBack())} />
return { headerLeft: back }
}
// ...
}
The next step is to override the hardware back button. For that we'll use the package react-navigation-backhandler:
// ...
import { AndroidBackHandler } from 'react-navigation-backhandler'
class MyPage extends Component {
// ...
onHardwareBackButton = () => {
showConfirmDialog(() => this.props.navigation.goBack())
return true
}
render () {
return (
<AndroidBackHandler onBackPress={this.onHardwareBackButton}>
{/* ... */}
</AndroidBackHandler>
)
}
}

Related

componentDidUpdate not being called

I have a react native app, and I am calling componentDidUpdate on App.js, but it doesn't fire.
I wonder if this is because I am calling from App.js?
Here is the App.js files:
class App extends Component {
componentDidUpdate = () => {
if (this.props.text && this.props.text.toString().trim()) {
Alert.alert(this.props.title || 'Mensagem', this.props.text.toString());
this.props.clearMessage();
}
}
render() {
return (
<NavigationContainer>
<Navigator />
</NavigationContainer>
)
}
}
const mapStateToProps = ({ message }) => {
return {
title: message.title,
text: message.text
}
}
const mapDispatchToProps = dispatch => {
return {
clearMessage: () => dispatch(setMessage({
title: '',
text: ''
}))
}
}
const connectDispatch = connect(mapStateToProps, mapDispatchToProps);
const connectApp = connectDispatch(App);
export default connectApp;
And here is where I am calling it.Inside a dispatch in posts action.
.then(res => {
dispatch(fetchPosts());
dispatch(postCreated());
dispatch(setMessage({
title: 'Sucesso',
text: 'Nova Postagem!'
}));
});
All other dispatchs are fired.
It's not the if that is preventing the alert to be fired, because I already put the alert outside of the if.
Change this
componentDidUpdate = () => { ... }
for this:
componentDidUpdate(prevProps, prevState, snapshot) { ... }
Keep in mind the componentDidUpdate does not trigger on first render
Thanks all!
I could fix it.
Instead of importing from '.ActionTypes' I was importing from 'Message'
import { SET_MESSAGE } from '../actions/ActionTypes';
I am new to Redux and it caught me offguard!

How to pass localization info from this.context in react component to its child consts?

I have implemented localization for React-native app according to this file as LocalizationContext.js:
import React from 'react';
import Translations, {DEFAULT_LANGUAGE} from '../constants/Translations';
import AsyncStorage from '#react-native-community/async-storage';
import * as RNLocalize from 'react-native-localize';
const APP_LANGUAGE = 'appLanguage';
export const LocalizationContext = React.createContext({
Translations,
setAppLanguage: () => {},
appLanguage: DEFAULT_LANGUAGE,
initializeAppLanguage: () => {},
});
export const LocalizationProvider = ({children}) => {
const [appLanguage, setAppLanguage] = React.useState(DEFAULT_LANGUAGE);
const setLanguage = language => {
Translations.setLanguage(language);
setAppLanguage(language);
AsyncStorage.setItem(APP_LANGUAGE, language);
};
const initializeAppLanguage = async () => {
const currentLanguage = await AsyncStorage.getItem(APP_LANGUAGE);
if (!currentLanguage) {
let localeCode = DEFAULT_LANGUAGE;
const supportedLocaleCodes = Translations.getAvailableLanguages();
const phoneLocaleCodes = RNLocalize.getLocales().map(
locale => locale.languageCode,
);
phoneLocaleCodes.some(code => {
if (supportedLocaleCodes.includes(code)) {
localeCode = code;
return true;
}
});
setLanguage(localeCode);
} else {
setLanguage(currentLanguage);
}
};
return (
<LocalizationContext.Provider
value={{
Translations,
setAppLanguage: setLanguage,
appLanguage,
initializeAppLanguage,
}}>
{children}
</LocalizationContext.Provider>
);
};
and it works fine in different screens but App.js file which is something like:
const MainTabs = createBottomTabNavigator(
{
Profile: {
screen: ProfileStack,
navigationOptions: {
// tabBarLabel: Translations.PROFILE_TAB,
},
},
HomePage: {
screen: HomeStack,
navigationOptions: {
tabBarLabel: Translations.HOME_TAB,
},
},
},
{
initialRouteName: 'HomePage'
},
},
);
export default class App extends Component {
// static contextType = LocalizationContext;
render() {
// const Translations = this.context.Translations;
// console.log(Translations.PROFILE_TAB);
return (
<LocalizationProvider>
<SafeAreaView style={{flex: 1}}>
<AppNavigator />
</SafeAreaView>
</LocalizationProvider>
);
}
}
I do access Translation in App component as you can find them in commented lines, but how can I pass related information to some const like tab titles? Translations.PROFILE_TAB is undefined.
I ended up changing this into a service.
Also I use redux and pass the store in to get user preferences and set them:
import * as RNLocalize from 'react-native-localize';
import { saveUserOptions } from '../actions/login';
import LocalizedStrings from 'react-native-localization';
export const DEFAULT_LANGUAGE = 'ar';
let _reduxStore;
function setStore(store){
_reduxStore = store
}
const _translations = {
en: {
WELCOME_TITLE: 'Welcome!',
STEP1: 'Step One',
SEE_CHANGES: 'See Your Changes',
CHANGE_LANGUAGE: 'Change Language',
LANGUAGE_SETTINGS: 'Change Language',
BACK: 'Back'
},
ar: {
WELCOME_TITLE: 'صباحك فُل!',
...
}
};
let translation = new LocalizedStrings(_translations);
const setAppLanguage = language => {
translation.setLanguage(language);
_reduxStore.dispatch(saveUserOptions('user_language',language))
};
const initializeAppLanguage = async () => {
const currentLanguage = _reduxStore.getState().login.user_language
if (!currentLanguage) {
let localeCode = DEFAULT_LANGUAGE;
const supportedLocaleCodes = translation.getAvailableLanguages();
const phoneLocaleCodes = RNLocalize.getLocales().map(
locale => locale.languageCode,
);
phoneLocaleCodes.some(code => {
if (supportedLocaleCodes.includes(code)) {
localeCode = code;
return true;
}
});
setAppLanguage(localeCode);
} else {
setAppLanguage(currentLanguage);
}
};
export default {
setStore,
translation,
setAppLanguage,
initializeAppLanguage
}
I need to first setup things in my top main component:
LocalizationService.setStore(store)
...
// add the below to componentDidMount by which time persisted stores are populated usually
LocalizationService.initializeAppLanguage()
where I need to get strings I do:
import LocalizationService from '../../utils/LocalizationService';
....
static navigationOptions = () => {
// return here was needed otherwise the translation didn't work
return {
title: LocalizationService.translation.WELCOME_TITLE,
}
}
EDIT
to force update of title you will need to set a navigation param:
this.props.navigation.setParams({ otherParam: 'Updated!' })
** Further Edit **
The props navigation change hack only works for the current screen, if we want to refresh all screens we need to setParams for all screens. This could possibly be done using a listener on each screen or tying the screen navigationOptions to the redux state.
I'm using NavigationService (see https://reactnavigation.org/docs/en/navigating-without-navigation-prop.html) so I've created the following function to run through my screens and setparam on all of them and force an update on all:
function setParamsForAllScreens(param, value) {
const updateAllScreens = (navState) => {
// Does this route have more routes in it?
if (navState.routes) {
navState.routes.forEach((route) => {
updateAllScreens(route)
})
}
// Does this route have a name?
else if (navState.routeName) {
// Does it end in Screen? This is a convention we are using
if (navState.routeName.endsWith('Screen')) {
// this is likely a leaf
const action = NavigationActions.setParams({
params: {
[param]: value
},
key: navState.key,
})
_navigator.dispatch(action)
}
}
}
if (_navigator && _navigator.state)
updateAllScreens(_navigator.state.nav)
}

App exit on back click on android in react native?

on Android Mi Note 3, hardware back button is not fire the handleBackPress , when I will click on back the app exit.
I have do the following code but the handleBackPress is not called.
componentDidMount() {
BackHandler.addEventListener('hardwareBackPress', this.handleBackPress);
}
componentWillUnmount() {
BackHandler.removeEventListener('hardwareBackPress', this.handleBackPress);
}
handleBackPress = () => {
this.goBack(); // works best when the goBack is async
return true;
}
Navigation Code :
const ModalSignUp = createStackNavigator(
{
Signup: { screen: Signup, key: 'Signup' },
PartyList: { screen: PartyList, key: 'PartyList' },
StatesList: { screen: StatesList, key: 'StatesList' },
},
{
initialRouteName: 'Signup',
headerMode: 'none',
mode: 'card',
}
);
Navigate :
this.props.navigation.push("StatesList")
Expected :
back click on hardware button, go to previous screen.
Your error can be in the way you get the next view of react-navigation.
You need to use .push to create a new view on the stack and when you click the back button, the .goBack() will be triggered.
By default, back button will always make the navigation to go back on the stack, but if you have only one view in the stack (this happens when you only use .navigate) the app will exit.
Not sure how you are navigating through the views, but this can be a solution.
Edit: To solve this problem, when navigating through views, use navigation.push('viewname') instead of navigation.navigate('viewname'). You don't need any other method (like the one you put in the question).
Also check the docs to understand the how navigating works or this question
Try using return false instead of return true.
1. Import
import { BackHandler, DeviceEventEmitter } from 'react-native'
2. constructor
constructor(props) {
super(props)
this.backPressSubscriptions = new Set()
}
3. Add and Remove Listeners
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()
}
4. Handle back
handleHardwareBack = () => {
this.props.navigation.goBack(null)
console.log(" ********** This is called ************ ");
return true;
}
Try this:
import {BackHandler} from 'react-native';
export default class Component extends Component {
_didFocusSubscription;
_willBlurSubscription;
constructor(props) {
super(props);
this._didFocusSubscription = props.navigation.addListener('didFocus',payload =>
BackHandler.addEventListener('hardwareBackPress', this.onBackButtonPressAndroid)
);
}
}
componentDidMount() {
this._willBlurSubscription = this.props.navigation.addListener('willBlur', payload =>
BackHandler.removeEventListener('hardwareBackPress', this.onBackButtonPressAndroid)
);
}
componentWillUnmount() {
this._didFocusSubscription && this._didFocusSubscription.remove();
this._willBlurSubscription && this._willBlurSubscription.remove();
}
onBackButtonPressAndroid = () => {
//code when you press the back button
};
Give it a try... this one works for me: in componentWillUnmount
BackHandler.removeEventListener('hardwareBackPress', () => {});
Also, make sure in each case you check in your this.goBack(); it return something
goback = () => {
if (condition2)
// handling
return something;
if (condition2)
// handling
return something;
// default:
return true;
};

React native show a strange behavior. Can someone explain?

I'am creating a simple application with authentication. To change a state using redux with react-native-navigation (v1). For example, index.js
...
import { Navigation, } from 'react-native-navigation';
import { Provider, } from 'react-redux';
import store from './src/store';
import registerScreens from './src/screens';
registerScreens(store, Provider);
class App {
constructor () {
this.auth = false;
store.subscribe(this.onStoreUpdate.bind(this));
this.start();
}
onStoreUpdate () {
const state = store.getState();
if (this.auth != state.auth) {
this.auth = state.auth;
this.start();
}
}
start () {
switch (this.auth) {
case false:
Navigation.startTabBasedApp({
tabs: [{
screen: 'navigation.AuthScreen',
}, {
screen: 'navigation.RegisterScreen',
},],
});
break;
case true:
Navigation.startSingleScreenApp({
screen: {
screen: 'navigation.MainScreen',
},
});
break;
}
}
}
const application = new App();
Store is listening an update and change an application layout if need.
AuthScreen show a simple ActivityIndicator, when server request is perform. For example, auth.js
...
import { bindActionCreators, } from 'redux';
import { connect, } from 'react-redux';
import * as actions from './../actions';
...
class AuthScreen extends Component {
constructor (props) {
super(props);
this.state = {
loading: false,
...
};
this.handlePressEnter = this.handlePressEnter.bind(this);
}
handlePressEnter () {
...
this.loadingState(true);
jsonFetch(url, {
method: 'POST',
body: JSON.stringify({...}),
}).then((value) => {
this.loadingState();
this.props.actions.auth(true);
}).catch((errors) => {
this.loadingState();
console.log('Error while auth', errors);
});
}
...
loadingState (state = false) {
this.setState({
loading: state,
});
}
render () {
return (<View>
...
<Modal visible={this.state.loading} transparent={true} animationType="none" onRequestClose={() => {}}>
<View>
<ActivityIndicator size="large" animating={this.state.loading} />
</View>
</Modal>
</View>);
}
}
function mapStateToProps (state, ownProps) {
return {};
}
function mapDispatchToProps (dispatch) {
return {
actions: bindActionCreators(actions, dispatch),
};
}
export default connect(mapStateToProps, mapDispatchToProps) (AuthScreen);
I'am starting application with iOS simulator and try to authenticate. It show me activity indicator, then indicator disappear, but layout does not change. And strange behavior, if I comment this.loadingState(true); and this.loadingState(); in auth.js layout changes with success.
Can someone explain to me, why layout does not change from auth to main when activity indicator using?
I think that you can use dispatch props for loading.
For example When you call this.props.actions.auth(true);
You can return loading reducers.
handlePressEnter () {
...
dispatch({ type:'loading', loading: true });
jsonFetch(url, {
method: 'POST',
body: JSON.stringify({...}),
}).then((value) => {
dispatch({ type:'loading', loading: false });
this.props.actions.auth(true);
}).catch((errors) => {
this.loadingState();
console.log('Error while auth', errors);
});
}
And than you can use
<ActivityIndicator size="large" animating={this.props.loading} />
But dont forget the reducers return

React Native - Device back button handling

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!
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.
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);
};
}, []);
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.
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;
};
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
})
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
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);
}, []);
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 /> }
}
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,
})
}
/>
);
}
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
useFocusEffect(
React.useCallback(() => {
const onBackPress = () => {
navigation.navigate('Journal');
return true;
};
BackHandler.addEventListener('hardwareBackPress', onBackPress);
return () => {
BackHandler.removeEventListener('hardwareBackPress', onBackPress);
};
}, []),
);
`
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);
};
}, [])
);
}
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;