Force the screen defined in initialRouteName to be displayed when the tab button is pressed - react-native

I have a problem with bottom tab navigation. This is screen structure:
Main Tab:
Tab A
Screen A1
Tab B
Screen B1
Screen B2
I can navigate from Screen A1 to Screen B2 (changing tab).
I can navigate from Screen B1 to Screen B2.
This is the example code:
import React from "react";
import { StyleSheet, View, Text, Button } from "react-native";
import { NavigationContainer } from "#react-navigation/native";
import { createStackNavigator } from "#react-navigation/stack";
import { createBottomTabNavigator } from "#react-navigation/bottom-tabs";
const Tab = createBottomTabNavigator();
const Stack = createStackNavigator();
const TabAScreen1 = ({ route, navigation }) => {
return (
<View>
<Text>Tab A screen 1</Text>
<Button
title="Go to tab B screen 2"
onPress={() => {
navigation.navigate("TabBStack", { screen: "TabBScreen2" });
}}
/>
</View>
);
};
const TabBScreen1 = ({ route, navigation }) => {
return (
<View>
<Text>Tab B screen 1</Text>
<Button
title="Go to tab B screen 2"
onPress={() => {
navigation.navigate("TabBScreen2");
}}
/>
</View>
);
};
const TabBScreen2 = ({ route, navigation }) => {
return (
<View>
<Text>Tab B screen 2</Text>
</View>
);
};
function TabBStack() {
return (
<Stack.Navigator initialRouteName="TabBScreen1" screenOptions={{ headerShown: false }}>
<Stack.Screen name="TabBScreen1" component={TabBScreen1}/>
<Stack.Screen name="TabBScreen2" component={TabBScreen2}/>
</Stack.Navigator>
);
}
function MainTab() {
return (
<Tab.Navigator screenOptions={{ headerShown: false }}>
<Tab.Screen name="TabAScreen1" component={TabAScreen1}/>
<Tab.Screen name="TabBStack" component={TabBStack}/>
</Tab.Navigator>
);
};
const App = props => {
const navigationRef = useRef();
return (
<NavigationContainer>
<MainTab/>
</NavigationContainer>
);
};
const styles = StyleSheet.create({});
export default App;
Navigation packages:
"#react-navigation/bottom-tabs": "^6.2.0",
"#react-navigation/native": "^6.0.8",
"#react-navigation/stack": "^6.1.1",
At the beginning, if I navigate to Tab B using bottom tab button, Screen B1 is loaded. Then I can navigate to Screen B2 from Screen B1 or Screen A1. Being in Screen B2, if I tap on Tab B bottom tab button the Screen B2 is popped and Screen B1 is showed. This is the expected behavior.
The problem is this one. At the beginning, if I navigate to Screen B2 from Screen A1, Screen B2 is loaded. At this point, I can't navigate to screen B1 at all. If I tap on Tab B bottom tab button nothing happens, even initialRouteName of TabBStack is set to TabBScreen1. I think in this case the first screen in the stack is B2.
Is it possible to pop B2 (or all screens in the stack) and push B1 tapping bottom tab button? I know about listeners, but I don't know how to implement this behavior.
function MainTab() {
return (
<Tab.Navigator screenOptions={{ headerShown: false }}>
<Tab.Screen name="TabAScreen1" component={TabAScreen1}/>
<Tab.Screen name="TabBStack" component={TabBStack}
listeners={({ navigation }) => ({
tabPress: (e) => {
console.log("Tab B pressed");
// If current route is not Screen B1, then pop all and push Screen B1
},
})}
/>
</Tab.Navigator>
);
};

This only happens if you initially load the application start in stack A and have never been to stack B before. This causes the initial route of the TabBStack to not be mounted!
If you then navigate to a nested screen inside the stack of the TabBStack from TabAScreen1, the default route reset functionality does not work. The framework does only mount TabBScreen2 but not the initial route specified in that particular tab.
If we do the following instead:
Load the application: the default screen is TabAScreen1
Go to TabBStack using the bottom tab navigation.
Go back to TabAScreen1.
Click the Go to tab B screen 2 button
Press the TabBStack bottom tab button
You will notice that it now successfully resets the route. This is the default behavior used in most applications.
How do we solve this?
We have luck. The framework allows us to do this in a very convenient way by using the initial prop.
By default, when you navigate a screen in the nested navigator, the specified screen is used as the initial screen and the initial route prop on the navigator is ignored.
In our case, this causes the TabBScreen1 to never be mounted. Thus, we can not reset to this route. If we provide initial: false while navigating to the nested screen, the navigation framework will not use TabBScreen2 as the initial route but TabBScreen1. If we do this, we can reset the route by pressing the TabBStack button again.
navigation.navigate("TabBStack", { screen: "TabBScreen2", initial: false });

Related

Using both DrawerNavigator and BottomTabNavigator with the same screens

I'm writing a React Native app, while using react-navigation package for navigating through the app screens.
On my app's home screen, there is a bottom tab bar with about 4-5 buttons, each leading to a different screen. Besides that, all of my app's screens contain a navigation drawer that leads to the rest of the screens. All the screens listed on the bottom tab bar are included in the navigation drawer as well.
App.js:
const App = () => {
return (
<NavigationContainer>
<DrawerNavigator />
</NavigationContainer>
)
}
DrawerNavigator.js:
const DrawerNavigation = createDrawerNavigator();
const DrawerNavigator = () => {
return (
<DrawerNavigation.Navigator>
<DrawerNavigation.Screen
name="ScreenA"
component={BottomTabNavigator} />
<DrawerNavigation.Screen
name="ScreenB"
component={ScreenB} />
<DrawerNavigation.Screen
name="ScreenC"
component={ScreenC} />
</DrawerNavigation.Navigator>
)
}
BottomTabNavigator.js:
const BottomTabNavigation = createBottomTabNavigator();
const BottomTabNavigator = () => {
return (
<BottomTabNavigation.Navigator>
<BottomTabNavigation.Screen
name="ScreenA"
component={ScreenA} />
<BottomTabNavigation.Screen
name="ScreenB"
component={ScreenB} />
</BottomTabNavigation.Navigator>
)
}
My question is how can I sync between them?
Let's say on the navigation drawer I have ScreenA, ScreenB, and ScreenC, while on the bottom tab bar I have only Screen A and Screen B. I want to click on ScreenB in the drawer, and have ScreenB selected as well on the tab bar, and vice versa, clicking on ScreenB on the tab bar and have it selected too on the drawer.
Is such thing possible? How would you implement it?

React Native nested Tab and Stack Navigation weird behavior: stack only works if I press on tabs when application first opens

I'm developing an application in React Native with the following layout:
A footer tab bar with 3 screens: screen1, screen2, screen3.
Additionally, I nested another screen in screen 1 and another in screen 2 with stack navigation.
When Screen2 tab is pressed, Screen2 opens with some default data.
However, a second button in Screen 1 that opens Screen2 with user entered data.
The function in Screen1 that goes to Screen2 is:
function onPressHandler(){
navigation.navigate('Screen2', {data: X});
}
And data in then retrieved on Screen2.js :
const information = !route.params? 'default' : route.params.data;
Now here's what happens: when the application opens, if I go to Screen 2 via Tab Navigation (by pressing on the footer) then when I am on Screen1 I can also access screen 2 via button (with user entered data) as many times as I like.I can switch screens as many times as I like and it works all the time.
However, if I do not do this, and just go to screen2 via Screen1 button, never pressing on Screen2 tab in the footer, I get the following error:
The action 'NAVIGATE' with payload {"name":"Screen2","params":{"data: xxx"}} was not handled by any navigator.
Here's a a visual rapresentation of the application layout (hope it helps):
Screen 1 | Screen 2(data: default) | Screen 3
On Press: nestedScreen1 on Press: nestedScreen2
On Press: Screen2(data: enteredByUser)
Here's the code:
import Screen1 from './Screen1.js';
import Screen2 from './Screem2.js'
import Screen3 from './screen3.js';
import NestedScreen1 from './NestedScreen1.js';
import NestedScreen2 from './NedstedScreen2.js';
import { NavigationContainer } from '#react-navigation/native';
import {createNativeStackNavigator} from '#react-navigation/native-stack';
import { createBottomTabNavigator } from '#react-navigation/bottom-tabs';
export default function App() {
const Tab = createBottomTabNavigator();
const Stack = createNativeStackNavigator();
const StackNavigationForScreen1= () => {
return(
<Stack.Navigator>
<Stack.Screen name="Screen1" component={Screen1} />
<Stack.Screen name="NestedScreen1" component={NestedScreen1} />
</Stack.Navigator>
);}
const StackNavigationForScreen2 = () => {
return(
<Stack.Navigator >
<Stack.Screen name='Screen2' component={Screen2} />
<Stack.Screen name='NestedScreen2' component={NestedScreen2} />
</Stack.Navigator>
);}
return (
<NavigationContainer>
<Tab.Navigator>
<Tab.Screen name='Screen1Tab' component={StackNavigationForScreen1}/>
<Tab.Screen name='Screen2Tab' component={StackNavigationForScreen2}/>
<Tab.Screen name='Screen3' component={Screen3}/>
</Tab.Navigator>
</NavigationContainer>
);}
Any idea what I might have done wrong or how to open Screen2 from Screen1 without having to press on Screen2 tab at least once first?
to navigate b/w nested stack you have to use something like this:-
navigation.navigate('nested_stack_screen', {
screen: 'screen1',
params: {
name:'anything'
},
})

How do I add a navigation button to a React Navigation Stack header with nested Bottom Tab Navigator?

I am trying to build a mobile app in react-native and I'm having some problems setting up React Navigation.
What I want to achieve is a Bottom Tab Navigator that Navigates to the 'Home' screen and the 'Profile' Screen. From the 'Home' screen, there should be a button to navigate to the 'Settings' screen in the Header.
I have got to the point where I have a Bottom Tab Navigator that can successfully navigate between the 'Home' and 'Profile' screens, as well as a button on the header for the Settings screen using the Stack navigation header. However, I am having trouble navigating to the 'Settings' screen with this button.
My code for the Stack navigator is:
const MainStackNavigator = () => {
return (
<Stack.Navigator screenOptions={screenOptionStyle}>
<Stack.Screen
name="Home"
component={HomeScreen}
options = { ({navigation}) => ({
title: "Home",
headerStyle: {
backgroundColor: '#ff6600',
},
headerRight: () => (
<Button
onPress={() => navigation.navigate(SettingScreen)}
title="Settings"
color="#fff"
/>
)
})}
/>
<Stack.Screen name="Settings" component={SettingScreen} />
</Stack.Navigator>
);
}
When I click on the Settings button, I get the error:
"The action 'NAVIGATE' with payload undefined was not handled by any navigator.
Do you have a screen named 'SettingScreen'?"
Upon looking for a solution to this error I found this article: Nesting Navigators
It recommends keeping nested navigators to a minimal. Is my method even the right way about going for this UI design? Is there a way to achieve this with only using one navigator?
After some time trying to solve this I found the problem was quite silly of me. navigation.navigate takes the name of the screen to navigate to, but I was giving it the component.
To fix the problem I changed
onPress={() => navigation.navigate(SettingScreen)}
to
onPress={() => navigation.navigate('Settings')}
Add this below your render method!
render () {
const { navigate } = this.props.navigation;
}
And then in the onPress
onPress={() => navigate(SettingScreen)}
Hopefully this helps

Set Screen Title in ReactNative createBottomTabNavigator

I am trying to set the Screen Title of the Navigated Screen by using navigator. But it's not working/ changing and doesn't throw an exception either.
Scenario
I want to load some states from Redux and display nice Header Title in the Navigation Screen when the user clicks on the bottom Tab.
User clicks on second Tab in bottom navigation tab
Load state from Redux
Set Header Title
Expected
I want to set the following text to the Screen title
headerTitle set in Screen
Actual
I declared my BottomTabNavigator as following:
import FilterScreen from '../screens/FilterScreen';
import ItemsListScreen2 from '../screens/ItemsListScreen copy';
const BottomTab = createBottomTabNavigator();
const INITIAL_ROUTE_NAME = 'List';
export default function BottomTabNavigator({ navigation, route }) {
// this code works
navigation.setOptions({ headerTitle: 'header Set in Navigator' });
const [isFilterVisible, toggleFilter] = useState(false);
return (
<>
<BottomTab.Navigator
initialRouteName={INITIAL_ROUTE_NAME}
tabBarOptions={{
inactiveBackgroundColor: '#42a5f5',
inactiveTintColor: '#ffffff'
}}
>
<BottomTab.Screen
name="List"
component={ItemsListScreen}
options={{
title: 'List',
tabBarIcon: ({ focused }) => <TabBarIcon focused={focused} name="md-calendar" iconType="ion" />,
}}
/>
<BottomTab.Screen
name="List2"
headerMode="screen"
component={ItemsListScreen2}
screen
options={{
title: 'title List2',
headerTitle: 'my header title',
tabBarIcon: ({ focused }) => <TabBarIcon focused={focused} name="md-calendar" iconType="ion" />,
}}
/>
....
</BottomTab.Navigator>
</>
);
}
In the above component, I could set the Screen Title by using navigation.setOptions and it works.
So, I tried to do the same operation to set the headerTitle in List2 Screen. But it doesn't have any effect to the screen header. It only changes the label of the tab to "this is a title".
import { StyleSheet, View, Text } from 'react-native';
const ItemsListScreen2 = ({ navigation }) => {
navigation.setOptions({
headerTitle: `headerTitle set in Screen`, // <-- no effect at the screen level
title: "this is a title"
});
return (
<View>
<Text>Screen 2</Text>
</View>
);
}
I tried to do console.log to navigation object passed to the component and I can see it here:
Could you please help me to sort out this issue and suggest me how I could set the title from the screen?
Do I need to create a new function (Eg. getHeaderTitle), load the Redux state and set the title due to the route within BottomTabNavigation component? I want to keep that component clean and I want to load the Redux States only when the user navigates to the screen. I'd like to keep the screen specific codes in its own related screen.

React Native navigating between two screens back & forth

I have Home screen where we search for an ItemCode, after we search ItemCode it will get details from API & screen will navigate to Screen 1(Item Details Screen) & from Screen 1 we have a search for ItemNutrients and after search it will get Nutrients & Navigate to Screen 2 (Nutrients Screen)
In other words
Home -> Screen 1 (Item Details Screen) -> Screen 2 (Nutrients Screen)
After getting the necessary details from API from search for Item Details & Nutrients, User can navigate between Item Details & Nutrients back and forth..
I could navigate from Screen 1 (item Details) to Screen2 (Nutrients Screen) and swipe back to Screen 1 (item Details) but how could I swipe forward to look at Screen 2 (Nutrients Screen) again without searching for nutrients in Screen 1 as I already have the data from API for Nutrients.
Any ideas on how to implement this ? this is basically navigate to Screen 1 to Screen 2 on search from Screen 1 or if Screen 2 search is already done and we have data so swipe forward should navigate to Screen 2, I can have a button to navigate to Screen 2, the button on Screen 1 conditinally appears only when we have API data for Nutrients Screen and it will navigate to Screen 2 on that button click, but i need swipe functionality as well to navigate forward
I have Home, Screen 1, Screen 2 in stackNavigator, is it alright or i could use any other navigators to achieve this.
I have researched how to do this and can't find it in the docs anywhere. Please let me know if I am missing it somewhere. Thanks!
Nutrition Context:
import React, { createContext, useState, useEffect } from "react";
export const NutriSyncContext = createContext();
const DEFAULT_IS_NUTRI_SCANNED = false;
export const NutriSyncContextProvider = ({ children }) => {
const [isNutritionScanned, setisNutritionScanned] = useState(
DEFAULT_IS_NUTRI_SCANNED
);
// When we first mount the app we want to get the "latest" conversion data
useEffect(() => {
setisNutritionScanned(DEFAULT_IS_NUTRI_SCANNED);
}, []);
const nutrionScanClick = ({ navigation }) => {
setisNutritionScanned(true);
//navigation.navigate("NutritionFacts");
};
const contextValue = {
isNutritionScanned,
setisNutritionScanned,
nutrionScanClick,
};
return (
<NutriSyncContext.Provider value={contextValue}>
{children}
</NutriSyncContext.Provider>
);
};
inside navigation.js
const tabPNMS = createMaterialTopTabNavigator();
function tabPNMSStackScreen() {
const { isNutritionScanned } = useContext(NutriSyncContext);
console.log(isNutritionScanned);
return (
<tabPNMS.Navigator initialRouteName="PNMSNutrition" tabBar={() => null}>
<tabPNMS.Screen name="PNMSNutrition" component={PNMSNutrition} />
{isNutritionScanned ? (
<tabPNMS.Screen name="NutritionFacts" component={NutritionFacts} />
) : null}
</tabPNMS.Navigator>
);
}
and In stack Navigation:
const SearchStack = createStackNavigator();
const SearchStackScreen = () => (
<NutriSyncContextProvider>
<SearchStack.Navigator
initialRouteName="Search"
screenOptions={{
gestureEnabled: false,
headerStyle: {
backgroundColor: "#101010",
},
headerTitleStyle: {
fontWeight: "bold",
},
headerTintColor: "#ffd700",
headerBackTitleVisible: false, //this will hide header back title
}}
headerMode="float"
>
<SearchStack.Screen
name="Search"
component={Search}
options={({ navigation }) => ({
title: "Search",
headerTitleAlign: "center",
headerLeft: () => (
<Entypo
name="menu"
size={24}
color="green"
onPress={() => navigation.dispatch(DrawerActions.toggleDrawer())}
/>
),
})}
/>
<SearchStack.Screen
name="NutriTabs"
options={() => ({
title: "PNMS",
headerTitleAlign: "center",
})}
component={tabPNMSStackScreen}
/>
</SearchStack.Navigator>
</NutriSyncContextProvider>
);
I have NutriSyncContextProvider around the stack, but when I click the button on my page it will call nutrionScanClick method in context class it works just fine but when i do navigation.navigate to the NutritionFacts tab that is enabled i get error saying NutritionFacts tab doesn't exist
I got it working below is the code:
const nutrionScanClick = () => {
if (!isNutritionScanned) setisNutritionScanned(true);
else {
//Make a API call here for new data for the new scanned one's and then navigate
navigation.navigate("NutritionFacts");
}
};
useEffect(() => {
if (isNutritionScanned) {
navigation.navigate("NutritionFacts");
}
}, [isNutritionScanned]);
There's no swipe back n forth between screens in a stack navigator. You need to use a tab navigator to be able to keep multiple screens like that.
https://reactnavigation.org/docs/material-top-tab-navigator
import { createMaterialTopTabNavigator } from '#react-navigation/material-top-tabs';
const Tab = createMaterialTopTabNavigator();
function MyTabs() {
return (
<Tab.Navigator tabBar={() => null}>
<Tab.Screen name="ItemDetails" component={ItemDetails} />
{showNutrients ? <Tab.Screen name="Nutrients" component={Nutrients} /> : null}
</Tab.Navigator>
);
}