Share Screens between Tab Navigator and Drawer Navigator while preserving state - react-native

My app has 3 main screens (each of which is a Stack Navigator):
MessagesStack
ExpensesStack
CalendarStack
It also has 2 supplemental screens (again, both stacks):
DashboardStack
DocumentsStack
I want to combine a drawer navigator and a tab navigator, but with some overlap. In particular, what I'm trying to achieve is for the drawer navigator to include links to all of the stacks, while the bottom tab navigator contains links only to the three main stacks. The tab navigator should always be visible, on every screen - essentially a sticky footer that enables the user to quickly jump to any of the three main screens. So the layout is like this:
--- Messages
--- Expenses
--- Calendar
--- Dashboard
--- Documents
| Messages | Expenses | Calendar |
Pressing the Messages item in the drawer nav should take the user to the same view as pressing the Messages item from the tab nav. It's important that the state of that view be preserved. In particular, suppose the user opened a specific message in the messages view. This takes them to a MessageDetail screen inside the Messages stack. If they navigate away from the Messages stack and then navigate back, they should still see the MessageDetail screen no matter what navigation route they took.
I've tried a few approaches, but none of them quite work. The closest I got was something like this:
function BottomTabNavigator({ initialRouteName }) {
return (
<BottomTab.Navigator initialRouteName={props.initialRouteName}>
<BottomTab.Screen name="MessagesTab" component={MessagesStackNavigator} />
<BottomTab.Screen name="ExpensesTab" component={ExpensesStackNavigator} />
<BottomTab.Screen name="CalendarTab" component={CalendarStackNavigator} />
<BottomTab.Screen
name="DashboardTab"
component={DashboardStackNavigator}
options={{
tabBarShowLabel: false,
headerShown: false,
tabBarButton: () => <></>,
}}
/>
<BottomTab.Screen
name="DocumentsTab"
component={DocumentsStackNavigator}
options={{
tabBarShowLabel: false,
headerShown: false,
tabBarButton: () => <></>,
}}
/>
</BottomTab.Navigator>
);
}
Then my root navigator was like this:
<NavigationContainer>
<Drawer.Navigator>
<Drawer.Screen
name="MessagesDrawer"
options={{ title: "Messages" }}
children={() => <BottomTabNavigator initialRouteName="MessagesTab" />}
/>
<Drawer.Screen
name="ExpensesDrawer"
options={{ title: "Expenses" }}
children={() => <BottomTabNavigator initialRouteName="ExpensesTab" />}
/>
<Drawer.Screen
name="CalendarDrawer"
options={{ title: "Calendar" }}
children={() => <BottomTabNavigator initialRouteName="CalendarTab" />}
/>
<Drawer.Screen
name="DashboardDrawer"
options={{ title: "Dashboard" }}
children={() => <BottomTabNavigator initialRouteName="DashboardTab" />}
/>
<Drawer.Screen
name="DocumentsDrawer"
options={{ title: "Documents" }}
children={() => <BottomTabNavigator initialRouteName="DocumentsTab" />}
/>
</Drawer.Navigator>
</NavigationContainer>
Unfortunately, this creates 5 independent TabNavigators which don't share state. So if e.g. the user navigates to Drawer(Messages) -> Tab(Calendar), then navigates away, the next time they tap on Drawer(Messages) it will take them to the Calendar tab! If I set unmountOnRender={true} it solves that problem: tapping Drawer(Messages) will now always open the Messages tab. But the stack state is now lost, so when e.g. the user opens a message, navigates away, and comes back, the message is no longer open.
If it's possible to achieve what I'm trying to do here declaratively that would be ideal, but even if the only way is imperative (e.g. with some navigation.jumpTo calls inside effects in each screen component) that would still be ok. I've tried some imperative approaches but nothing that works yet. Any help is greatly appreciated.

Related

React-native going back from a nested navigation container does not work

I am creating a nested navigation container in react-native and the stack looks like this:
-Main Navigation Container:
- Home
- Market Navigation Container:
- Market
- Cart
- About
When I go to home or about and go back, it works properly. However, when I go back from Market or cart (Which I expect to go to the Home page) it shows an error saying:
The action 'POP' with payload {"count":1} was not handled by any navigator.
Is there any screen to go back to?
This is a development-only warning and won't be shown in production.
This is my code for the main navigation container:
<NavigationContainer independent={true}>
<Stack.Navigator>
<Stack.Screen options={myOptions} name="Home" component={Home} />
<Stack.Screen
options={{ headerShown: false }}
name="MarketNavigation"
component={MarketNavigation}
/>
<Stack.Screen options={myOptions} name="About" component={About} />
</Stack.Navigator>
</NavigationContainer>
And this is my code for the market navigation:
<NavigationContainer ref={navigationRef} independent={true}>
<Stack.Navigator>
<Stack.Screen
options={myOptions}
name="Market"
component={Market}
/>
<Stack.Screen
options={myOptions}
name="Cart"
component={Cart}
/>
</Stack.Navigator>
</NavigationContainer>
I faced this when navigating to nested navigator using "screen" option like this:
navigation.navigate('Root', {
screen: 'Settings'
})
;
As mentioned in the documentation:
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.
To solve this you should use "initial: false" like so:
navigation.navigate('Root', {
screen: 'Settings',
initial: false,
});
Could you add in the code where you navigate from Home to MarketNavigation and back?

Navigate to the screen when Tab on BottomTabNavigator is pressed

I would like to navigate to the screen when the particular tab on the BottomTabNavigator is pressed.
Normally, when the tab is pressed, it navigates to the configured screen automatically. But I don't want to have that behaviour. I want to hide the bottom tab on that screen and provide back feature in the top bar too. I normally use navigation.navigate('routeName') in ReactNavigationStack screens. But I don't know how/where to write this code in the BottomTabNavigator configuration.
For example, I've got the following 5 tabs in the bottom bar. I want to navigate to AddNewScreen when Add button is pressed. I don't know where to put that onPress event. I tried to put it under options and BottomTab.Screen. But still no luck.
I tried to intercept onPress event to use navigation.navigate. But it's not even hit and it always opens the AddNewScreen with the tab bar.
<BottomTab.Navigator initialRouteName={INITIAL_ROUTE_NAME}>
<BottomTab.Screen
name="Home"
component={HomeScreen}
initialParams="Home Params"
options={{
title: 'Home',
tabBarIcon: ({ focused }) => <TabBarIcon focused={focused} name="md-home" iconType="ion" />,
}}
/>
<BottomTab.Screen
name="AddNew"
component={AddNewScreen}
options={{
title: 'Add',
tabBarIcon: ({ focused }) => <TabBarIcon focused={focused} name="md-add-circle" iconType="ion"
onPress={(e) => {
e.preventDefault();
console.log(e)
}} />,
}}
/>
</BottomTab.Navigator>
The Add new screen is always opened with the bottom tab bar.
Questions:
Is there anyway to navigate to specific screen when the tab is
pressed?
Is there anyway to hide the bottom tab bar on that Add New
Screen?
Update:
The Navigation library v6 supports the Listener feature that can be used
<Tab.Screen
name="Chat"
component={Chat}
listeners={{
tabPress: e => {
// Prevent default action
e.preventDefault();
//Any custom code here
alert(123);
},
}}
/>;
You can have a custom functionality in the bottom toolbar using the tabbar button. The code would be like below
<Tab.Screen
name="Settings2"
component={SettingsScreen}
options={{
tabBarButton: props => (
<TouchableOpacity {...props} onPress={() => alert(123)} />
),
}}
/>
This would render a normal bottom tab bar button but the onclick would show the alert, you can replace the code with your navigate or any other code you need.
Also the 'SettingsScreen' component can be a dummy component returning null.
Hope this helps.
You can have a custom functionality
<Tab.Screen
name="Add"
component={View}
listeners={({ navigation }) => ({
tabPress: (e) => {
// Prevent default action
e.preventDefault();
// Do something with the `navigation` object
navigation.navigate("PhotoNavigation"); // Here!!!!!!!!!!!!!!!!!!!!!!!!!!!!
},
})}
/>
<Tab.Screen name="Notifications" component={Notifications} />

React navigation 5.x / React native – bottom-tabs navigation with same instance of component

I want to reuse the same instance of one component in two tabs (bottom bar tabs).
created with const Tab = createBottomTabNavigator();
Tab stack:
<Tab.Navigator
tabBarOptions={{
activeTintColor: Colors.tabs.active,
inactiveTintColor: Colors.tabs.inactive,
}}>
<Tab.Screen
name="NavigationMap"
component={Map}
options={{
tabBarLabel: 'Navigation',
}}
/>
<Tab.Screen
name="DiscoveryMap"
component={Map}
options={{
tabBarLabel: 'Discover',
}}
/>
<Tab.Screen
name="Other"
component={OtherComponent}
options={{
tabBarLabel: 'Other',
}}
/>
</Tab.Navigator>
I want to have the same behavior than in the Google Maps application on Android with the "Explore" and "Commute" tabs: stay in the same screen with a different state. I do not want to reload completely my map between the 2 tabs (and have independant zoom levels, center, ...).
Note: I cannot achieve that behavior with the tabPress method.
You can add focus listeners to both screens. Here, you can set the context or global state that can be accessed by the HomeNavigation component and change the behaviour.
<Tab.Screen
name="Home"
component={HomeNavigation}
listeners={{
focus: () => console.warn('focused 1'),
}}
/>
<Tab.Screen
name="Home2"
initialParams={{testing: true}}
component={HomeNavigation}
listeners={{
focus: () => console.warn('focused 2'),
}}
/>

How to implement a button on the left side of the header in react-navigation

I have a main Stack navigator(X) and inside that I have screen which takes me to another stack navigator(A). But when I go stack(A) it's not showing me the button to go back to stack(X). I do understand that using multiple stack navigators like these is not the best practice, but I want to implement a Tab navigator inside of stack(A) and each of the tabs would contain a stack navigator.
I have tried implementing a button as described on the react-navigation docs, but it doesn't describe how to implement it on the left side of the header.
How do I get around this?
You can use the same code provided in the docs only change is that instead of headerRight you have to use headerLeft but this will also change the back behavior, check the docs for more details.
<Stack.Screen
name="Home"
component={HomeScreen}
options={{
headerTitle: props => <LogoTitle {...props} />,
headerLeft: () => (
<Button
onPress={() => alert('This is a button!')}
title="Info"
color="#fff"
/>
),
}}
/>

React native: How can I have multiple drawer navigator links point to screens within the same stack navigator

I am new to react native and I haven't seen this question asked by anyone or haven't found a way around this.
Using react navigation 5 with expo.
Currently I have a the following app structure:
Stack navigator inside of drawer navigator.
Example of page structure:
Drawer Navigator ( links ):
Home (RouteStack)
Screen 1
Screen 2
Screen 3
RouteStack( screens) :
Home ( initial route )
Screen 1
Screen 2
Screen 4
How can I get Screen 1/Screen 2 link in drawer navigator load RouteStack: Screen 1/Screen 2?
These links are provided to easily jump to the required screen.
Need some guidance on how to achieve this.
I have thought of the possibility of drawer inside of stack, but there are screens inside of drawer that may not be listed in the stack. Hence, went with stack inside of drawer.
I have also tried to do a navigation.navigate(route.name) inside of RouteStack
Sample code:
Drawer navigator:
<NavigationContainer>
<Drawer.Navigator drawerContent={(props, navigation) => <CustomDrawerContent {...props} {...navigation} />}>
<Drawer.Screen name="Home" component={RouteStack} />
<Drawer.Screen name="MyItems" component={RouteStack} />
<Drawer.Screen name="ContactRep" component={RouteStack} />
<Drawer.Screen name="Settings" component={SettingInfo} />
</Drawer.Navigator>
</NavigationContainer>
Stack navigator (RouteStack) looks like this:
<Stack.Navigator
initialRouteName="Home"
screenOptions={{ gestureEnabled: false, headerTitleAlign: 'auto' }}
// headerMode="float"
>
<Stack.Screen
name="Home"
component={HomeScreen}
options={{
title: '',
headerStyle: {
backgroundColor: '#fff',
},
headerTintColor: '#000',
headerTitleStyle: {
fontWeight: 'bold'
},
headerLeft: props => <HeaderLeftMenu {...props} />,
headerRight: props => <HeaderRightMenu {...props} />,
headerTitle: props => <HeaderTitle {...props} />
}}
/>
<Stack.Screen
name="ContactRep"
component={ContactRep}
options={{ headerTitle: props => <HeaderTitle {...props} /> }}
/>
<Stack.Screen
name="MyItems"
component={MyItems}
options={{ headerTitle: (props, navigation) => <HeaderTitle {...props} /> }}
/>
</Stack.Navigator>
Thanks in advance and help is appreciated.
Your method is fine. But to clarify your ideas I will give you an example.
Assume I have a main Drawer. In that drawer I can navigate to 2 different screens. Inside those screens, I can navigate and do diferent things (like going to sub-screens), but never go outside the drawer.
To do this, we would have to created nested navigators. This meaning, one type of navigator if going to be inside another one. In our case of example:
<Papa Drawer>
<Screen 1 component={StackSon1}>
<Screen 2 component={StackSon2}>
<Papa Drawer>
And then StackSon1, for example, will look like this:
StackSon = () => {
return (
<Stack.Navigator>
<Stack.Screen>
<Stack.Screen>
...
)
}
React Navigation will also handle every drawer separately, meaning that you don't have to worry about the user creating an infinite chain of open screens.
Also, remember that, when we Nest navigators using a function (like I did) we must use return (or the simplified version of return with just parenthesis)
Hope it helps.