The default setup
If you are building a React Native application using React Navigation and Redux, you will need to add a global handler for the Android hardware back button. You would put it on your top-level component, such as the App.js
component as the following:
import { BackHandler } from 'react-native'
import { NavigationActions } from 'react-navigation'
class ReduxNavigation extends React.Component {
componentDidMount() {
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() {
// ...
}
}
In componentDidMount
you add a event listener to the hardwareBackPress
event, which calls the onBackPress
function.
If you are not at the root index (nav.index === 0), you then dispatch a back action, and return true to stop the app from exiting, otherwise return false to exit the app.
Then you remove the event listener inside componentWillUnmount
so there won’t be memory leak.
The problem
That was all easy and simple. However, what if on a pariticular screen you want to handle the back button press a bit differently. For example, on a screen you have a form, and you want to alert the user when they tap the back button if the form is dirty. You will notice that the default setup won’t work because it doesn’t perform such check.
The solution
Let’s say the screen you want to have a custom back button handler is called Form. We want to display an alert when the user taps the back button and the form is dirty. We will just do the same setup in the Form component, add a custom hardwareBackPress
event handler inside componentDidMount
as the following:
componentDidMount() {
BackHandler.addEventListener('hardwareBackPress', this.onBackPress)
}
componentWillUnmount() {
BackHandler.removeEventListener('hardwareBackPress', this.onBackPress)
}
onBackPress = () => {
const { goBack } = this.props // eslint-disable-line
const { isDirty } = this.state // eslint-disable-line react/prop-types
if (isDirty) {
Alert.alert(
'Unsubmitted Changes',
'You have unsubmitted form changes that will be lost if you go back, are you sure you want to leave?',
[
{ text: 'Cancel', style: 'cancel' },
{
text: 'OK',
onPress: () => {
dispatch(NavigationActions.back())
return true
},
},
],
{ cancelable: false },
)
return false
}
dispatch(NavigationActions.back())
return true
}
In the custom back button handler we check if the Form is dirty, and if it is we will display the alert and only navigate back when the user taps “OK”.
The tricky bit
Now if you try it out you will notice that the alert does get shown, but it still navigates back. Why? Because remeber the top-level component is still alive, it is just hidden in the navigation stack, and its back handler also listens to the event. I firstly tried using React Navigation’s lifecycle hooks willBlur
and didBlur
, but they didn’t work 100% of the times.
This is where we can get a little help from Redux. We can add a state property to our Redux store called globalBackHandlerEnabled
, which defaults to true
. The idea is that in the custom onBackPress
function inside the Form component, we will set the globalBackHandlerEnabled
to false
when we enter the screen, and in the global back button press handler, we can check the value of globalBackHandlerEnabled
, and return right away if its value is false
, otherwise we will continue. Then when the Form screen unmounts, we set globalBackHandlerEnabled
back to true, and our global back button hanlder will be active again.
Just a note you will need to implement an action creator and a reducer to toggle the value of globalBackHandlerEnabled
in your Redux store, I will skip this part as it would be the same with other Redux goodies.
In the global back button handler, we will just add the following lines at the very top of the function:
const { globalBackHandlerEnabled } = this.props
if (!globalBackHandlerEnabled) return true
Then in the Form component, we will toggle the status of the globalBackHandlerEnabled
state property:
componentDidMount() {
this.props.onSetGlobalBackHandlerStatus(false)
BackHandler.addEventListener('hardwareBackPress', this.onBackPress)
}
componentWillUnmount() {
this.props.onSetGlobalBackHandlerStatus(true)
BackHandler.removeEventListener('hardwareBackPress', this.onBackPress)
}
That’s it, now it will prevent the back navigation when the user tries to navigate away from a dirty form while still maintain the default behaviour.