Reputation: 310
Situation: The react native app has a BottomTabNavigator (react-navigation/material-bottom-tabs) and one of the tabs has a NativeStackNavigator (react-navigation/native-stack).
BottomTabNavigator:
tab1
tab2
tab3:
NativeStackNavigator:
screen1
screen2
Problem:
When I press tab3 (from the BottomTabNavigator) I see screen 1.
If I press a button on screen 1 then it will navigate from screen 1 to screen 2.
When I press tab1 or tab2 and then I press tab3 again then I see screen 1.
I want to see screen 2 (because that's what should be at the top of the stack from the NativeStackNavigator right?).
Did the stack from NativeStackNavigator reset?
Did the whole NativeStackNavigator render again?
What causes this behavior?
Code BottomTabNavigator:
export type MainNavigationBarParam = {
tab1: undefined;
tab2: undefined;
tab3: undefined;
};
const Tab = getMainNavigationBarTabNavigator();
export const tab3Stack = createNativeStackNavigator();
export function MainNavigationBar() {
const sizeToUse = 25;
return (
<Tab.Navigator
screenOptions={defaultScreenOptions()}
barStyle={{backgroundColor: theme.colors.primary}}>
<Tab.Screen
name="tab1"
component={Component1}
/>
<Tab.Screen
name="tab2"
component={Component2}
/>
<Tab.Screen
name="tab3"
component={Component3}
/>
</Tab.Navigator>
);
}
function defaultScreenOptions() {
const screenOptions: any = {
headerShown: false,
tabBarHideOnKeyboard: true,
};
if (Platform.OS === 'web') {
screenOptions.swipeEnabled = false;
}
return screenOptions;
}
Code Component3:
export type Tab3Stack ParamList = {
Screen1: undefined;
Screen2: {id: string; name: string};
};
export default function Component3() {
const [topComponentHeight, setTopComponentHeight] = useState(0);
function onLayout(event: LayoutChangeEvent) {
if ('top' in event.nativeEvent.layout) {
const withTop = event.nativeEvent.layout as unknown as {top: number};
setTopComponentHeight(withTop.top);
}
event;
}
return (
<View style={navContainerStyle(topComponentHeight).navContainer} onLayout={onLayout}>
<tab3Stack.Navigator screenOptions={{headerShown: false}}>
<tab3Stack.Screen
name={'Screen1'}
component={Screen1}
/>
<tab3Stack.Screen
name={'Screen2'}
component={Screen2}
/>
</tab3Stack.Navigator>
</View>
);
}
Reproduced the code above for 2 tabs: https://snack.expo.dev/@jacobdel/182747 The problem does not occur on snack, only in my app.
No errors or warnings.
Sometimes this is shown, but after disabling the quill editor it doesn't appear anymore
Cause is found: getMainNavigationBarTabNavigator();
from BottomTabNavigator
File MainNavigationBarTabNavigator.ts looks like this:
import {createMaterialBottomTabNavigator} from '@react-navigation/material-bottom-tabs';
import {MainNavigationBarParam} from './MainNavigationTabs';
export function getMainNavigationBarTabNavigator() {
return createMaterialBottomTabNavigator<MainNavigationBarParam>();
}
While MainNavigationBarTabNavigator.web.ts looks like this:
import {createMaterialTopTabNavigator} from '@react-navigation/material-top-tabs';
import {MainNavigationBarParam} from './MainNavigationTabs';
export function getMainNavigationBarTabNavigator() {
return createMaterialTopTabNavigator<MainNavigationBarParam>();
}
Snack only works as intended when using MainNavigationBarTabNavigator.ts
Stuck..
Replacing the code from MainNavigationBarTabNavigator.web.ts with the code from MainNavigationBarTabNavigator.ts does not show the intended behavior as shown in the Snack example.
Upvotes: 4
Views: 1699
Reputation: 9856
There seems to be an issue with the navigation event handling of @react-navigation/material-top-tabs
.
If we handle this on our own, then the nested stack is not reset. We can prevent the default navigation action by using a tabPress
event listener and calling e.preventDefault
.
In your case, this is done as follows for screen2
(which is the nested stack).
<Tab.Screen
name="screen2"
component={Screen2}
listeners={({ navigation, route }) => ({
tabPress: (e) => {
e.preventDefault();
navigation.navigate({ name: route.name, merge: true });
},
})}
/>
Notice that this works fine on the phone, but has some issues on the web. If we navigate fast between screen2
and screen1
multiple times, then the navigation does not work at all. I haven't found the root cause for this issue.
However, we can make this work on the web as well as on the phone by providing a custom top navigation bar using the tabBar
prop of the tab navigator. We override the default behavior of the tabPress
function.
The original component is exported and is named MaterialTopTabBar. Sadly, it does not provide the possibility to provide a custom onTabPress
function via props.
I have forked the component and 'fixed' (without knowing what exactly is going wrong here) the onTabPress
function.
import {
useTheme,
} from '@react-navigation/native';
import Color from 'color';
import * as React from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { TabBar, TabBarIndicator } from 'react-native-tab-view';
export default function FixedTabBarTop({
state,
navigation,
descriptors,
...rest
}) {
const { colors } = useTheme();
const focusedOptions = descriptors[state.routes[state.index].key].options;
const activeColor = focusedOptions.tabBarActiveTintColor ?? colors.text;
const inactiveColor =
focusedOptions.tabBarInactiveTintColor ??
new Color(activeColor).alpha(0.5).rgb().string();
return (
<TabBar
{...rest}
navigationState={state}
scrollEnabled={focusedOptions.tabBarScrollEnabled}
bounces={focusedOptions.tabBarBounces}
activeColor={activeColor}
inactiveColor={inactiveColor}
pressColor={focusedOptions.tabBarPressColor}
pressOpacity={focusedOptions.tabBarPressOpacity}
tabStyle={focusedOptions.tabBarItemStyle}
indicatorStyle={[
{ backgroundColor: colors.primary },
focusedOptions.tabBarIndicatorStyle,
]}
indicatorContainerStyle={focusedOptions.tabBarIndicatorContainerStyle}
contentContainerStyle={focusedOptions.tabBarContentContainerStyle}
style={[{ backgroundColor: colors.card }, focusedOptions.tabBarStyle]}
getAccessibilityLabel={({ route }) =>
descriptors[route.key].options.tabBarAccessibilityLabel
}
getTestID={({ route }) => descriptors[route.key].options.tabBarTestID}
onTabPress={({ route }) => {
const event = navigation.emit({
type: 'tabPress',
target: route.key,
canPreventDefault: true,
});
if (!event.defaultPrevented) {
navigation.navigate({ name: route.name, merge: true });
}
}}
onTabLongPress={({ route }) =>
navigation.emit({
type: 'tabLongPress',
target: route.key,
})
}
renderIcon={({ route, focused, color }) => {
const { options } = descriptors[route.key];
if (options.tabBarShowIcon === false) {
return null;
}
if (options.tabBarIcon !== undefined) {
const icon = options.tabBarIcon({ focused, color });
return (
<View style={[styles.icon, options.tabBarIconStyle]}>{icon}</View>
);
}
return null;
}}
renderLabel={({ route, focused, color }) => {
const { options } = descriptors[route.key];
if (options.tabBarShowLabel === false) {
return null;
}
const label =
options.tabBarLabel !== undefined
? options.tabBarLabel
: options.title !== undefined
? options.title
: route.name;
if (typeof label === 'string') {
return (
<Text
style={[styles.label, { color }, options.tabBarLabelStyle]}
allowFontScaling={options.tabBarAllowFontScaling}
>
{label}
</Text>
);
}
return label({ focused, color });
}}
renderBadge={({ route }) => {
const { tabBarBadge } = descriptors[route.key].options;
return tabBarBadge?.() ?? null;
}}
renderIndicator={({ navigationState: state, ...rest }) => {
return focusedOptions.tabBarIndicator ? (
focusedOptions.tabBarIndicator({
state: state,
...rest,
})
) : (
<TabBarIndicator navigationState={state} {...rest} />
);
}}
/>
);
}
const styles = StyleSheet.create({
icon: {
height: 24,
width: 24,
},
label: {
textAlign: 'center',
textTransform: 'uppercase',
fontSize: 13,
margin: 4,
backgroundColor: 'transparent',
},
});
I have used it as follows.
export function MainNavigationBar() {
return (
<Tab.Navigator
tabBar={props => <FixedTabBarTop {...props} />}
screenOptions={defaultScreenOptions()}>
<Tab.Screen
name="screen1"
component={Screen1}
/>
<Tab.Screen
name="screen2"
component={Screen2}
/>
</Tab.Navigator>
);
}
I have updated your snack. You might want to submit an issue on GitHub as well. It feels like a bug.
Upvotes: 1