How to detect user leaving screen in React Native and act accordingly - react-native

How to detect user leaving a screen in React Native and act accordingly ?
For an example when user tries to leave current screen alert should popup and say You have unsaved changes, are you sure you want to leave?. If yes user can leave the screen, if no user should be in same screen.
import { useFocusEffect } from '#react-navigation/native';
import {Alert} from 'react-native'
const Profile = (props) => {
useFocusEffect(
React.useCallback(() => {
// Do something when the screen is focused
return () => {
// Do something when the screen is unfocused
// Useful for cleanup functions
Alert.alert(
'Want to leave',
'You have unsaved changes, are you sure you want to leave?',
[
{
text: 'yes',
onPress: () => {
// should leave to the screen user has navigate
},
},
{ text: 'no', onPress: () => null },
],
true
)
};
}, [])
);
return <ProfileContent />;
}
export default Profile
Currently I'm facing few problems with this code.
Alert will popup once user already navigate to selected navigation screen. But I want Alert to be popup before user navigate to the selected screen.
If user selected no, User should be remain in the same screen.
Is there a way to achieve these things ?
Thanks.
Answer
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}
/>
);
}
for more information refer: https://reactnavigation.org/docs/preventing-going-back/
This will prevent user to going back from one screen to another. But this does not prevent tab navigation because screen does not get removed.
prevent from tab navigation
<Tab.Screen
name={'search'}
component={SearchNavigator}
options={{
tabBarIcon: ({ focused, color }) => (
<View>
<Icon
name='search1'
color={
focused
? 'black'
: 'white'
}
size={25}
/>
</View>
),
}}
listeners={{
tabPress: (e) => {
if (true) {
// Prevent default action
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: () =>
navigationRef.current?.navigate(
'search',
{}
),
},
]
)
}
},
}}
/>
You can add listener tabPress like above code and provide an Alert.

Related

Show Card Overlay in React-Native

I want when the user is not verified to show an overlay at the Top of the screen that the user can click on close on verify button to get rid of it.
Also, I want it to show permanently above all screens until the user dismiss it.
How can I do that functionality ?
You can Alert in every screen. You can do it on your navigation folder by using <NavigationContainer> . I think for navigation , you already used it. So, add something on onReady and onStateChange of NavigationContainer.
Customise the Alert as your expectations. You can do the desired task inside onPress of Alert. In described code, the notVerify value is what you have set to check the user verification.
A demo is :
const routeNameRef = useRef<string | undefined>('');
const navigationRef = useNavigationContainerRef();
<NavigationContainer
ref={navigationRef}
onReady={() => {
routeNameRef.current = navigationRef.current?.getCurrentRoute()?.name;
if(notVerify){
Alert.alert(
"Alert Title",
"My Alert Msg",
[
{
text: "Cancel",
onPress: () => console.log("Cancel Pressed"),
style: "cancel"
},
{ text: "OK", onPress: () => console.log("OK Pressed") }
]
);
}
}}
onStateChange={async () => {
const previousRouteName = routeNameRef.current;
const currentRouteName = navigationRef.current?.getCurrentRoute()?.name;
if (previousRouteName !== currentRouteName) {
if(notVerify){
Alert.alert(
"Alert Title",
"My Alert Msg",
[
{
text: "Cancel",
onPress: () => console.log("Cancel Pressed"),
style: "cancel"
},
{ text: "OK", onPress: () => console.log("OK Pressed") }
]
);
}
}
routeNameRef.current = currentRouteName;
}}
>
<RootNavigator /> //This is the screens section(Stack.screen function) of your app. I did it inside a function name RootNavigator, so I passed this inside this.
</NavigationContainer>
);

BackHandler's HardwareBackPress event gets triggered on every screen of the navigator

I'm using BackHandler on the "home" screen of my app to alert confirmation to the user to exit the app. I've 2 sets of screen Authentication and Home on my root navigator and isLogged bool determines which set gets shown.
Problem: The first render of the app works fine(be it Auth or Home set of screens) but when isLogged is changed and the set of screens changes, BackHandler starts triggering on every screen of the changed set. This is only fixed after restarting the app. Working example - https://snack.expo.dev/#msaxena92/11fd51
Expected result: Pressing back inside a navigator should take you to the initialRoute or first screen of the navigator and only after that when there are no more screens in the navigation stack it exits the app.
You have 2 options:
Use the useFocusEffect hook instead of useEffect which will make sure that the effect is run only when you're on this screen:
useFocusEffect(
React.useCallback(() => {
const backAction = () => {
Alert.alert("Hold on!", "Are you sure you want to exit?", [
{ text: "Cancel" },
{ text: "Yes", onPress: () => BackHandler.exitApp() }
]);
return true;
};
const backHandler = BackHandler.addEventListener(
"hardwareBackPress",
backAction
);
return () => backHandler.remove();
}, [])
);
Alternatively, you can also check for focus inside the effect:
React.useEffect(() => {
const backAction = () => {
if (!navigation.isFocused()) {
return false;
}
Alert.alert("Hold on!", "Are you sure you want to exit?", [
{ text: "Cancel" },
{ text: "Yes", onPress: () => BackHandler.exitApp() }
]);
return true;
};
const backHandler = BackHandler.addEventListener(
"hardwareBackPress",
backAction
);
return () => backHandler.remove();
}, [navigation]);
Also see https://reactnavigation.org/docs/custom-android-back-button-handling/

How to navigate to another screen on back key press in react native?

I am trying to make an alert when the user clicks back button where he will be offered with two options, Yes or No. If the user clicks "No" the user will stay on that screen and if the user presses "Yes" then, I want some different screen to be shown.
Basically I want to prevent user from going back to the previous screen and instead redirect the user to some another screen.
Here is the example useEffect code that I am trying to make this work:
useEffect(() => {
navigation.addListener('beforeRemove', (e) => {
e.preventDefault();
Alert.alert(
'Registration Process',
'Are you sure you want to cancel the registration process?',
[
{
text: 'Yes',
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.navigate('SignUp'); // On pressing Yes, user should be shown this screen.
},
},
{text: 'No', style: 'cancel', onPress: () => {}},
],
);
});
}, [navigation]);
After running the app, when I press "Yes" I get treated with the alert box again and again.
You can create a hook and call it on backpress on when user tries to leave the page
Create a folder called hooks where your App.js is located.
Inside that create a file called useBackHandler.ts
Inside useBackHandler.ts paste this
import { useEffect } from 'react';
import { BackHandler } from 'react-native';
export function useBackHandler(handler: () => boolean) {
useEffect(() => {
BackHandler.addEventListener('hardwareBackPress', handler);
return () => BackHandler.removeEventListener('hardwareBackPress', handler);
}, [handler]);
}
Then in your RegisterScreen create a function to execute on backpress or when user wants to goBack like this
const AlertConfirmation = () => {
Alert.alert(
'Registration Process',
'Are you sure you want to cancel the registration process?',
[
{
text: 'Yes',
style: 'destructive',
onPress: () => {
navigation.navigate('ScreenOne');
},
},
{ text: 'No', style: 'cancel', onPress: () => {} },
]
);
};
I've created a Snack for you to see working example..
Check this out.

Detect when user moves to specific screens react-navigation

I am using react-navigation with react-native and I have bottomTabs for navigation, I have a screen to edit some info, I want to be able to detect when the user tries to move from that screen to other "specific" screens to trigger a certain action.
what I tried:
useEffect(
() =>
navigation.addListener('blur', (e) => {
// Prompt the user before leaving the screen
Alert.alert('You haven’t saved your changes,[
{ text: translate('yes'), style: 'cancel' },
{
text: translate('no'),
style: 'destructive',
onPress: () => navigation.navigate('EditProfileScreen'),
},
])
}),
[navigation]
)
But the above triggers when I move to any screen, while I want it only to be triggered for specific screens.
Any insights?
You should remove listener after lefting this component, so that it does not continue to other one:
const checkBeforeLeaving = (e) => {
// Prompt the user before leaving the screen
Alert.alert('You haven’t saved your changes,[
{ text: translate('yes'), style: 'cancel' },
{
text: translate('no'),
style: 'destructive',
onPress: () => navigation.navigate('EditProfileScreen'),
},
])
}
Then return with a clearing function in useEffect() to clear side-effects on component unmount:
useEffect(() => {
// Adding side effect on component mount
navigation.addListener('blur', checkBeforeLeaving);
// Specify how to clean up after this effect on component-unmount:
return () => navigation.removeEventListener('blur', checkBeforeLeaving)
}, [navigation])
In this way, this side-effect will be limited to this specific component only.

Dismiss Alert in React Native

So I have an app that shows an alert when you go idle for a few seconds, and there is 'YES' button in the alert when you pressed it it will refresh the timer and shows the alert again, and if i do nothing with the alert it goes to next page somehow the Alert still open in the next page and I want it to be closed. Is there anyway to close it? Thanks in advance
here is my code
componentDidMount(){
this.timer = setTimeout(this.sessionTimeout, 3000); //auto reset after 60 seconds of inactivity
}
componentWillUnmount(){
clearTimeout(this.timer);
}
sessionTimeout(){
this.timer = setTimeout(this.onSubmit, 3000)
Alert.alert('You ran out of time', 'still editing?',
[
{text: 'YES', onPress:this.resetTimer.bind(this)}
],
{cancelable: false}
)
}
resetTimer(){
clearTimeout(this.timer);
this.timer = setTimeout(this.sessionTimeout, 3000);
}
onSubmit() {
clearTimeout(this.timer);
this.setState({ submitted: true }); //shows activity indicator
const data = this.state;
const result = validate(data, this.props.validationLang);
if (!result.success) {
this.setState({ error: result.error });
this.setState({ submitted: false }); //terminate activity indicator
}
else
this.props.onSubmit(this.extractData(data), () => this.setState({ submitted: false }));
}
It is not possible to close Alert so you can change the scenario of your navigation by setting up "No" button inside alert and navigate to another screen only after pressing no button without pressing any action it should stay on this screen you can set button name to any thing instead of "No" which make sense to user then i can navigate to other screen if i don't want to refresh the timer by pressing yes.
You reset when the alert appears, you press OK, you press No, you want the screen to move.
Alert.alert('You ran out of time', 'still editing?',
[
{text: 'YES', onPress: () => this.resetTimer()},
{
text: "Cancel",
onPress: () => this.props.navigation.navigate("nextScreen"),
style: "cancel"
}
],
{cancelable: false}
)
You can use react-native-modalbox like this:
modal = null;
render(){
return (
<Modal
ref={(ref) => { this.modal = ref }}
coverScreen={true}
position={"center"}
onClosed={() => { console.log('modal closed') }}
swipeToClose={true}
backdropPressToClose={true} >
<View>
<Text>Message</Text>
<TouchableOpacity
onPress={() => { this.modal.close() }}
>
<Text>close</Text>
</TouchableOpacity>
</View>
</Modal>
)
}
Now you can show modal with this.modal.open() and close it with this.modal.open() and track closing modal with props of your Modal component.
For more information please read the Documentation.
I hope this can helo you.