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.
Related
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 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.
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.
To better understand how our users is using our app, we want to fire an event to analytics when a user swipe to the next tab, and a different event if the user clicks to navigate to the next tab.
The tabs are created with createMaterialTopTabNavigator from react-navigation-tabs.
const SwipeableTabs = createMaterialTopTabNavigator(
{
Tab1,
Tab2,
Tab3,
},
{
swipeEnabled: true,
}
);
I've tried listening to the following event, but the payload does not contain any information about how the user navigated (swipe vs click).
this.props.navigation.addListener('didFocus', payload => console.log(payload))
Is there any way to know if the user swiped or clicked, so that I can fire the appropriate event to analytics?
You could try adding a press listener to the tab bar as described here.
The demo code looks like this:
const MyTabs = TabNavigator({
...
}, {
tabBarComponent: TabBarBottom /* or TabBarTop */,
tabBarPosition: 'bottom' /* or 'top' */,
navigationOptions: ({ navigation }) => ({
tabBarOnPress: (scene, jumpToIndex) => {
console.log('onPress:', scene.route);
jumpToIndex(scene.index);
},
}),
});
Now you can simply set a flag in your tabBarOnPress function and check for it in your didFocus listener. If the flag has been set then you know it was via tab bar press, otherwise it must be a swipe. Note that this assumes you don't manually set the tab on your own somewhere. But if you do, you could just set the flag in that press handler as well.
tabBarOnPress docs
There is listeners on screens, you can use swipeStart, swipeEnd or tabPress callbacks to fire your events to the analytics.
<Tab.Screen
name="Chat"
component={Chat}
listeners={({navigation, route}) => ({
tabPress: (e) => {
// call your analytics events
},
swipeStart: (e) => {
// call your analytics events
},
swipeEnd: (e) => {
// call your analytics events
},
})}
/>
From my observation, the Alert dialog seems built on top of the React Native app.
So it pops out everytime you call it, and doesn't to be in the render function.
The catch is it is not an async task so the code after Alert will continue to execute regardless the callback function.
The code below demonstrates a situation where the Alert dialog keeps popping out because it reads the same barcode over and over again.
(It is written in TypeScript. Just take my word, this is a valid snippet.)
import * as React from "react";
import Camera from "react-native-camera";
import { Alert } from "react-native";
export default class BarcodeScanSreen extends React.Component<any ,any> {
private _camera;
private _onBarCodeRead = e => {
if (e.type === "QR_CODE") {
Alert.alert(
"QRCode detected",
"Do you like to run the QRCode?",
[
{ text: "No", onPress: this._onNoPress },
{ text: "Yes", onPress: this._onYesPress }
],
{ cancelable: false }
);
}
};
private _onYesPress = () => { /* process the QRCode */ }
private _onNoPress = () => { /* close the alert dialog. */ }
render() {
return (
<Camera
onBarCodeRead={this._onBarCodeRead}
aspect={Camera.constants.Aspect.fill}
ref={ref => (this._camera = ref)}
>
{/* Some another somponents which on top of the camera preview... */}
</Camera>
);
}
}
Is there a way to pause the JS code and await the response from Alert?
React-native Alert doesn't stop the execution of code below it. By changing it to async function which resolves the promise on user action will work as ASYNC-Alert.
const AsyncAlert = async () => new Promise((resolve) => {
Alert.alert(
'info',
'Message',
[
{
text: 'ok',
onPress: () => {
resolve('YES');
},
},
],
{ cancelable: false },
);
});
await AsyncAlert();
Use react-native-alert-async
I've just published a package that does exactly this and permit to await for the choice of the user. It is compatible with Expo.
import AlertAsync from "react-native-alert-async";
const myAction = async () => {
const choice = await AlertAsync(
'Title',
'Message',
[
{text: 'Yes', onPress: () => 'yes'},
{text: 'No', onPress: () => Promise.resolve('no')},
],
{
cancelable: true,
onDismiss: () => 'no',
},
);
if (choice === 'yes') {
doSomething();
}
else {
doSomethingElse();
}
}
Original answer: I've made a PR to ReactNative for this feature: https://github.com/facebook/react-native/pull/20312
Here's a simple solution.
The trick used here is to make a function that calls the button's onPress function, then resolve the promise with the index of the button. Note that this requires the alert to be uncancellable.
showAsyncAlert = (title, message, buttons, options) => {
return new Promise((resolve, reject) => {
// We can't detect a cancellation, so make sure the Alert is not cancellable.
options.cancellable = false
buttons.forEach((button, index) => {
let onPress = button.onPress
button.onPress = option => {
if (onPress) {
onPress(option)
}
resolve(index)
}
})
Alert.alert(title, message, buttons, options)
})
}
Usage:
let option = await showAsyncAlert(title, message, buttons options)
if (option === 0) {
foo()
} else {
bar()
}
Alert does not pause the code. In this case JS is not the only problem - the Camera component also keeps running in the background which is native and it will trigger the onBarCodeRead listener, regardless if the Alert is present or not.
You could try to stop the camera at the beginning on _onBarCodeRead with the stopPreview() method mentioned in the docs.
Also note that react-native-camera is currently in a migration process from Camera (RCTCamera) to RNCamera and in the new RNCamera I don't see a stopPreview() method. Anyhow, a simple flag would also do the job.
I have some workaround for this,If you have alert function like below
Alert.alert(
'Delete comment?',
'Are you sure you want to delete this comment?',
[
{
text: 'Cancel',
onPress: () => console.log('Cancel Pressed'),
style: 'cancel',
},
{ text: 'yes', onPress:() => this.props.deleteComment(commentId),
],
{ cancelable: false },
);
//call after comment is deleted
refreshPage();
This code does not wait for alert's response, it will execute refreshPage() immediately.
so, you can do something like
Alert.alert(
'Delete comment?',
'Are you sure you want to delete this comment?',
[
{
text: 'Cancel',
onPress: () => console.log('Cancel Pressed'),
style: 'cancel',
},
{ text: 'yes', onPress: async () => {await this.props.deleteComment(commentId);refreshPage();},
],
{ cancelable: false },
);
If you need to return a promise:
const asyncAlert = (title, message, callback) => (new Promise((resolve) => {
Alert.alert(
title,
message,
[{ text: "Cancel" }, { text: "OK", onPress: () => resolve() }], { cancelable: false }
);
}).then(callback));
Usage:
return asyncAlert("My Title", "Are you sure?", () => SOME_PROMISE_FUNCTION())