Differentiate between swipe and click navigation in tabs in react-navigation - react-native

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
},
})}
/>

Related

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

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.

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.

Tab bar navigation without component screen render

Is there a way to press on tab bar and not render component screen? i have been passing null function but it still renders blank screen. i want that wherever you press it stays on home screen.
Add a listener to the Screen.
component={() => null}
listeners={() => ({
tabPress: (e) => {
e.preventDefault(); // Prevents navigation
// Your code here for when you press the tab
},
})}
You can avoid navigation to the screen by creating your custom tabBar component in which you can handle onPress to each tabBarComponent so you can avoid navigation and do some action instead. Take a closer look at the example from react-navigation documentation here:
https://reactnavigation.org/docs/bottom-tab-navigator/#tabbar
This line of code from the example should be helpful for you:
const onPress = () => {
const event = navigation.emit({
type: 'tabPress',
target: route.key,
canPreventDefault: true,
});
if (!isFocused && !event.defaultPrevented) {
navigation.navigate(route.name);
}
};

How do we implement Scroll to top functionality on tapping of the corresponding tab button on a bottom tab navigator in react navigation?

The React Navigation version I am using is v5. In the ScrollView corresponding to a Bottom Tab Icon, if the user is already on that given screen, I want to enable functionality where the user scrolls to the top when this icon is pressed.
As stated in the documentation, this feature should be already implemented. But I think you have placed your ScrollView inside a nested StackNavigator, right?
In that case, you probably need to subscribe to TabNavigator event and fire your scrollToTop manually
React.useEffect(() => {
const unsubscribe = navigation.addListener('tabPress', e => {
// Get your scrollView ref and dispatch scrollToTop
});
return unsubscribe;
}, [navigation]);
Hope it'll help you!
None of the solutions worked for me on the web, not using listener and not using useScrollToTop which is provided by react-navigation. Because I have nested stacks and navigators and also custom tabbar.
I solved the problem by setting a navigation param for scrolling to the top.
I have custom tabbar, and I needed to scroll to top on the homepage which was the first route of the tab stack.
So, in the homepage I set the scrollToTop parameter for the navigator:
const homelistRef = React.useRef<FlatList | null>(null)
useFocusEffect(
useCallback(() => {
navigation.setParams({
scrollToTop: () => homelistRef.current?.scrollToOffset({ offset: 0, animated: true }),
});
}, [navigation, homelistRef]),
);
return (
<FlatList
ref={homelistRef}
...{other Flatlist configs}
/>
)
And in my Tabbar component, I read this new param and execute it in the onPress function of the tabs:
interface IProps extends BottomTabBarProps {}
const TabBar: React.FC<IProps> = ({ state, navigation }) => {
const handleTabPress = useCallback(
(route) => async () => {
const currentRoute = state.routes?.[state.index];
if (
route === currentRoute?.name &&
state.routes?.[0].state?.routes?.[0]?.params?.scrollToTop
) {
state.routes[0].state.routes[0].params.scrollToTop();
}
},
[navigation, state],
);
return (
{render tab component}
)
}
And that's the ultimate solution for executing some action on active tabs.

How to make "Go back to initial StackNavigator Screen when TabNavigator's Tab is pressed"

Intro
Just like Facebook, Instagram, and any other mobile app, I want to implement 'go back to initial screen in Stacknavigator'
if user press the button,
it goes back to the very first page.
Simple Use Case
see TabNavigator
Goes to 'Feed' Tab
Goes to 'User' Screen
Goes to another 'User' Screen
PRESS the Main Tab Icon - 'Feed'}
Goes BACK to 'Feed' Tab // so you won't see the 'back' button
And please leave a comment if you don't understand this use case, I will draw its state flow for you
Code for the icon on my Main TabNavigator.
navigationOptions: ({ navigation }) => ({
header: null,
tabBarIcon: ({ focused }) => {
const { routeName } = navigation.state;
....
return (
<TochableWithoutFeedback onPress={()=>{navigation.goback(iconName)}>
// undefined is not a function when I press the Button
// deeper screen. (not getting any error on Main)
<Ionicons
name={iconName}
size={30}
style={{ marginBottom: -3 }}
color={focused ? Colors.tabIconSelected : Colors.tabIconDefault}
/>
<TochableWithoutFeedback>
);
},
actually, it depends on how many deep is your routing like Instagram 2 to 3 deep routing else are tabs
so you can reset your router or go back to the main router by using
this.props.navigation.goBack(null)
for eg.
Tab navigation child ahs Stack navigation so in Stack, you can do something like
// Anyone watching this, please check your react-navigation version
// I'm using ^1.0.0-beta.21.
// props for tabBarOnpress varies can be varied (Editor John Baek)
tabBarOnPress: ({scene, jumpToIndex}) => {
jumpToIndex(scene.index)
navigation.dispatch(NavigationActions.reset({
index: 0,
actions: [
NavigationActions.navigate({ routeName: 'home' }) // go to first screen of the StackNavigator
]
}))
}
so whenever someone press Home Tab then all route reset and you see Feed screen just like Instagram
TabNavigation
-Home
|
StakeNavigation
|
mainScreen //main of home
Subrouts
RouteToProfile //just like insta
-Search
|
StakeNavigation
|
mainScreen //main of search
searchResult //if people serch some specific
and go on... so reset route at stakeNavigation level of Tab
const SubHome = StackNavigator({
Home: { screen: Home },
Home2: { screen: Home2 },
Home3 : { screen: Home3 },
},{
navigationOptions:({navigation,screenProps})=>({
tabBarLabel: I18n.t('tab_car'),
tabBarIcon: ({ tintColor }) => (
<CustomIcon name={'Home'} width={28} height={30} fill={tintColor} />
),
tabBarOnPress: (tab, jumpToIndex) => {
jumpToIndex(tab.index)
navigation.dispatch(NavigationActions.reset({
index: 0,
actions: [
NavigationActions.navigate({ routeName: 'Home' }) // go to first screen of the StackNavigator
]
}))
}
}),
headerMode:'none'
});