Statically setting stack navigator's header title - react-native

Suppose I have 2 navigators as follow:
const StackNavigator = () => {
const theme = useTheme();
return (
<Stack.Navigator
initialRouteName="BottomTabs"
headerMode="screen"
screenOptions={{
header: Header,
}}>
<Stack.Screen
name="BottomTabs"
component={BottomTabs}
options={({route, navigation}) => {
console.log('get bottom tab title', route, navigation)
const routeName = route.state
? route.state.routes[route.state.index].name
: 'NOTITLE';
return {headerTitle: routeName};
}}
/>
</Stack.Navigator>
);
};
This Stack navigator loads BottomTabs which is another navigator:
const BottomTabs = props => {
const theme = useTheme();
const tabBarColor = theme.dark
? overlay(6, theme.colors.surface)
: theme.colors.surface;
return (
<Tab.Navigator
initialRouteName="TaskList"
shifting={true}
activeColor={theme.colors.primary}
inactiveColor={color(theme.colors.text)
.alpha(0.6)
.rgb()
.string()}
backBehavior={'initialRoute'}
sceneAnimationEnabled={true}>
<Tab.Screen
name="TaskList"
component={TaskListScreen}
options={{
tabBarIcon: ({focused, color}) => (
<FeatherIcons color={color} name={'check-square'} size={23} />
),
tabBarLabel: 'InboxLabel',
tabBarColor,
title: 'Inbo title',
}}
/>
<Tab.Screen
name="Settings"
component={SettingsScreen}
options={{
tabBarIcon: ({focused, color}) => (
<FeatherIcons color={color} name={'settings'} size={23} />
),
tabBarLabel: 'SeetingsLabel',
tabBarColor,
title: 'Settings title',
}}
/>
</Tab.Navigator>
);
};
I want to change Stack header title based on the screen loaded from BottomTabs. trying to pass the title option to individual screen in BottomTabs didn't work.
How can I change Stack navigator's title based on the screen loaded from the child?

You can customize headerTitle like this:
<Stack.Screen
name="BottomTabs"
component={BottomTabs}
options={({route}) => {
let title;
const routeName = route.state
? route.state.routes[route.state.index].name
: route.params && route.params.screen
? route.params.screen
: 'TaskList';
switch (routeName) {
case 'TaskList':
title = 'Tasks screen';
break;
case 'Settings':
title = 'Settings screen';
break;
default:
return routeName;
}
return {headerTitle: title};
}}
/>
important note: route.state is undefined before any navigate. after that, stack creats it's on navigation state and your screen name prop is available.
Reference

Related

React Native Drawer Navigation show headerLeft

I have a Drawer navigator which is inside a Stack navigator and I'd like to display a header. Currently I can display everything I want however because the header is defined at the Stack level the navigation inside the header is stack level not drawer level which is preventing me from opening the drawer.
Root stack
<Stack.Navigator
initialRouteName={"Splash"}
screenOptions={{}}
component={SplashScreen}
>
{ auth ?
<Stack.Screen name="Drawer" component={DrawerStack} options={({ navigation }) => ({
title: 'My App',
headerLeft: () => (
<HeaderLeft navigation={ navigation } />
),
headerRight: () => (
<HeaderRight navigation={ navigation } />
),
headerTitleAlign: 'center',
headerTintColor: 'white',
headerStyle: {
backgroundColor: '#5742f5'
},
})} />
:
<Stack.Screen name="Auth" component={AuthStack} options={{
headerShown: false
}}/>
}
</Stack.Navigator>
Drawer stack
<Drawer.Navigator options={{
headerShown: true,
headerLeft: () => (
<HeaderLeft navigation={ navigation } />
),
}}>
<Drawer.Screen
name="Conversations"
options={{
title: props.title,
}}
component={ChatListScreen}
/>
<Drawer.Screen
name="ChatRoom"
options={{
drawerLabel: () => null,
title: null,
drawerIcon: () => null
}}
component={ChatRoomScreen}
/>
</Drawer.Navigator>
Note in the drawer navigator the line with headerLeft does nothing and is there to show where I attempted to put it thinking it would work. I did think it might be overlaying the stack one so I commented out the stack one and it didn't work.
HeaderLeft
export default function HeaderLeft ({ navigation }) {
const openMenu = () => {
navigation.toggleDrawer();
}
return (
<View style={styles.header}>
<Icon name='menu' onPress={openMenu} size={28} style={styles.icon} color="white"/>
</View>
)
}
My question is how can I refactor this to enable me to have the HeaderLeft component work to open the drawer. I will be adding more screens so ideally something I don't have to pass to each screen but if that is what works I am good with it too.
Options in DrawerStack not work. I modified it:
<Drawer.Navigator
screenOptions={{
headerLeft: () => <HeaderLeft />,
}}>
// ...
</Drawer.Navigator>
And a little change in HeaderLeft:
import { useNavigation } from '#react-navigation/native';
function HeaderLeft() {
const navigation = useNavigation();
const openMenu = () => {
navigation.toggleDrawer();
};
// render your Button
}
Demo: https://snack.expo.dev/#pqv2210/0d613b

React Navigation Authentication Flow: The action 'NAVIGATE' with payload {"name":"HomeScreen"} was not handled by any navigator

A beginner at React Native here, trying to combine this doc with AWS Amplify Authentication to implement React Navigation Authentication Flow but I can't seem to figure out what's wrong. Whenever I click on the login button, this error appears.
Navigation Code (excluding imports):
const NavigationGeneral = () => {
const [user, setUser] = useState(undefined);
const checkUser = async () => {
try {
const authUser = await Auth.currentAuthenticatedUser({bypassCache: true});
setUser(authUser);
} catch (e) {
setUser(null);
}
}
useEffect(() => {
checkUser();
}, []);
if (user === undefined) {
return (
<View style = {{flex: 1, justifyContent: 'center', alignItems: 'center'}}>
<ActivityIndicator />
</View>
)
}
return (
<Stack.Navigator screenOptions={{headerShown: false}}>
{user ? (
<>
<Stack.Screen name = "Login" component = {LoginScreen} />
<Stack.Screen name = "SignUp" component = {SignUpScreen} />
<Stack.Screen name = "ConfirmEmail" component = {ConfirmEmailScreen} />
<Stack.Screen name = "ForgotPassword" component = {ForgotPasswordScreen} />
<Stack.Screen name = "NewPassword" component = {NewPasswordScreen} />
</>
): (
<Stack.Screen name = "HomeScreen" component = {HomeTabNavigator} />
)}
</Stack.Navigator>
);
};
//* TAB NAVIGATOR FOR APP SCREENS
const HomeTabNavigator = () => {
return (
<Tab.Navigator
initialRouteName="Home"
screenOptions={{
headerShown: false,
tabBarStyle: {backgroundColor: '#0052cc'},
tabBarInactiveTintColor: '#fff',
tabBarActiveTintColor: '#fff',
tabBarActiveBackgroundColor: '#006600'
}}>
<Tab.Screen
name="Home"
component={HomeScreen}
options={{
tabBarLabel: 'Home',
tabBarIcon: ({ color, size }) => (
<Ionicons name="home-outline" color={'#fff'} size={25} />
),
}}
/>
<Tab.Screen
name="Courses"
component={CourseScreen}
options={{
tabBarLabel: 'Courses',
tabBarIcon: ({ color, size }) => (
<Ionicons name="library-outline" color={'#fff'} size={25} />
),
}}
/>
<Tab.Screen
name="Profile"
component={ProfileScreen}
options={{
tabBarLabel: 'My Profile',
tabBarIcon: ({ color, size }) => (
<Ionicons name="person-outline" color={'#fff'} size={25} />
),
}}
/>
<Tab.Screen
name="Forensic Tools"
component={ForensicToolsScreen}
options={{
tabBarLabel: 'Tools List',
tabBarIcon: ({ color, size }) => (
<Ionicons name="list-outline" color={'#fff'} size={25} />
),
}}
/>
<Tab.Screen
name="Leaderboard"
component={LeaderboardScreen}
options={{
tabBarLabel: 'Leaderboard',
tabBarIcon: ({ color, size }) => (
<Ionicons name="podium-outline" color={'#fff'} size={25} />
),
}}
/>
<Tab.Screen
name="Settings"
component={SettingsScreen}
options={{
tabBarLabel: 'Settings',
tabBarIcon: ({ color, size }) => (
<Ionicons name="settings-outline" color={'#fff'} size={25} />
),
}}
/>
</Tab.Navigator>
);
};
export default NavigationGeneral;
LoginScreen.js code snippet:
const onLoginPressed = async data => {
if (loading) {
return;
}
setLoading(true);
try {
await Auth.signIn(data.username, data.password);
navigation.navigate("HomeScreen");
console.log("Login");
} catch (e) {
Alert.alert('Oops', e.message);
}
setLoading(false);
};
I understand that I'm not supposed to manually navigate using navigation.navigate() but rather to conditionally define the screens. But whenever I remove navigation.navigate("HomeScreen"); from the code, nothing happens when I press the login button. So I assume something is wrong with my conditioning in my NavigationGeneral code, I just can't seem to figure out the problem.
Some help or additional tips would be greatly appreciated, thanks in advance. Please let me know if more info is required.
When you try to navigate to the HomeScreen I assume the user is not yet set in the state and therefore the screen with the name HomeScreen does not exist yet, so the navigator has nowhere to go.
Try setting the user in the NavigationGeneral upon login, it should event redirect automatically without using navigation.navigate.
You should not trigger navigation manually when implementing authentication flow with react-navigation. After the successful login, user will be truthy value. This means conditional rendering inside of navigation will handle it automatically.
Moreover, in case of condition user===undefined do not render LoadingIndicator, instead, create a new loading state and render LoadingIndicator when the state loading becomes true.
And I believe it should be !user instead of user, since not authenticated user (user===undefined) will want to see Login, Signup etc. screens.
return (
<Stack.Navigator screenOptions={{headerShown: false}}>
// Here it should be !user
{!user ? (
<>
<Stack.Screen name = "Login" component = {LoginScreen} />
<Stack.Screen name = "SignUp" component = {SignUpScreen} />
<Stack.Screen name = "ConfirmEmail" component = {ConfirmEmailScreen} />
<Stack.Screen name = "ForgotPassword" component = {ForgotPasswordScreen} />
<Stack.Screen name = "NewPassword" component = {NewPasswordScreen} />
</>
): (
<Stack.Screen name = "HomeScreen" component = {HomeTabNavigator} />
)}
</Stack.Navigator>
);
const [checkAuthUserLoading, setCheckAuthUserLoading] = React.useState(false);
const user = useSelector((state) => state.user); // Retrieve user from redux store. (It can be mobx, zustand or any other state management.)
const checkUser = async () => {
try {
setCheckAuthUserLoading(true);
const authUser = await Auth.currentAuthenticatedUser({
bypassCache: true,
});
setCheckAuthUserLoading(false);
dispatch(setAuthUser(user)); // I would suggest to use state management such as redux or mobx instead of storing user in component state
} catch (e) {
dispatch(setAuthUserError(e)); // Here you do not need to setUser(undefined) since its already failed and will be stay as undefined
}
};
if (checkAuthUserLoading) {
return <LoadingIndicator />;
}

Can i disable tabBarBadge on click of tab screen?

I have used Tab.Navigator to display 4 bottom navigation icons, one of which is Notification Icon. I want to disable the tabBarBadge option when i click on it. How to do it?
Here is the code I used
<Tab.Screen
name="Notifications"
component={Notifications}
options={{
tabBarIcon: ({ focused }) =>
focused ? (
<Notification_icon />
) : (
<Notification_icon_inactive />
),
tabBarBadge: 2,
}}
/>
There is a way to do this using NavigationContainer's ref and controlling the visibility of the badges through some state variable. Listen for state event on the ref. In the handler of this event you get the name of the current route using ref.current?.getCurrentRoute()?.name. If this name is equal to the Notifications screen name then you set notificationBadgeVisible to false thus hiding the badges. Check the following code.
const App = () => {
const ref = React.useRef<NavigationContainerRef>(null);
const [notificationBadgeVisible, setNotificationBadgeVisible] = React.useState(true);
const navigationStateChangeHandler = () => {
if (ref.current?.getCurrentRoute()?.name === 'Notifications') {
setNotificationBadgeVisible(false);
}
}
React.useEffect(() => {
ref.current?.addListener('state', navigationStateChangeHandler);
return () => { ref.current?.removeListener('state', navigationStateChangeHandler); }
});
return (
<NavigationContainer ref={ref}>
<Tab.Navigator>
<Tab.Screen name="Home" component={HomeScreen} />
<Tab.Screen
name="Notifications"
component={Notifications}
options={{
tabBarIcon: ({ focused }) =>
focused ? (
<Notification_icon />
) : (
<Notification_icon_inactive />
),
tabBarBadge: (notificationBadgeVisible ? 2 : undefined)
}}
/>
</Tab.Navigator>
</NavigationContainer>
);
};

Dynamic items in Drawer nested in Stack Navigator react-navigation

i`m realy stumped about this.
I have a bottom tabs width 4 screens
BottomTabs.js
export const AllTabs = ({ navigation }) => {
return (
<Tab.Navigator initialRouteName="Home" tabBar={props => <BottomNavigation {...props} />}>
<Tab.Screen name="Home" component={HomeStack} options={{
icon: 'shopping-store'
}}/>
<Tab.Screen name="Catalog" component={CatalogStack} options={{
icon: 'pulse'
}}/>
<Tab.Screen name="Cart" component={CartStack} options={{
icon: 'shopping-basket'
}} />
<Tab.Screen name="Profile" component={ProfileStack} options={{
icon: 'person'
}}/>
</Tab.Navigator>
)
}
In first Screen named Home, in HomeStack:
export const HomeStack = () => {
return (
<Stack.Navigator screenOptions={screenOptions} >
<Stack.Screen name="Home" component={HomeScreen} />
<Stack.Screen name="SectionDrawer" component={SectionDrawer} />
<Stack.Screen name="Detail" component={DetailScreen} />
</Stack.Navigator>
)
}
I calling drawer component in second screen
Drawer
export const SectionDrawer = (props) => {
const { route, navigation } = props
const { list } = route.params
return (
<Drawer.Navigator drawerContentOptions={{ activeBackgroundColor: '#5cbbff', activeTintColor: '#ffffff' }} >
<Drawer.Screen name="Section" component={SectionScreen} options={{ route, navigation }}/>
</Drawer.Navigator>
)
}
And finally in SectionScreen i`m fetcing data with api by id
SectionScreen
export const SectionScreen = (props) => {
console.log(props);
const {route, navigation} = props
const { sectionID } = route.params
const [isLoading, setLoading] = useState(true);
const [productList, setProductList] = useState([]);
useEffect(() => {
fetch('url, {
method: 'GET'
})
.then((response) => response.json())
.then((json) => setProductList(json))
.catch((error) => console.error(error))
.finally(() => setLoading(false));
}, []);
return (
<Content padder>
<Text>section screen.</Text>
<Text>Requested id {sectionID}</Text>
{isLoading ? (
<Spinner />
) : (
<ProductCards list={productList} perLine={2} navigation={navigation}/>
)}
</Content>
)
}
I calling SectionScreen in drawer with this construction:
<CardItem cardBody button onPress={() => {
navigation.dispatch(
CommonActions.navigate({
name: 'SectionDrawer',
params: {
screen: 'SectionScreen',
params: {
title: card.title,
sectionID: card.id
}
},
})
);
}}>
All of this works ... but I need to push in drawer dynamic links fetched from api
I am tried: in Drawer
<Drawer.Navigator drawerContentOptions={{ activeBackgroundColor: '#5cbbff', activeTintColor: '#ffffff' }} >
{list.map((item, index) => <Drawer.Screen name={item.title} component={SectionScreen} key={index}/>)}
</Drawer.Navigator>
and in place where I calling SectionScreen:
<CardItem cardBody button onPress={() => {
navigation.dispatch(
CommonActions.navigate({
name: 'SectionDrawer',
params: {
list,
screen: card.title,
params: {
title: card.title,
sectionID: card.id
}
},
})
);
}}>
After this I see items that I passed in list param,
but when i try to navigate to them, i have error in SectionScreen route.param.id is not defined, and route.params is undefined too.
Also i trying to fetch data that I need in drawer, in drawer component, and draw it with
drawerContent={props => <CustomDrawerContent {...props} />}
and render them with:
{catalogSections.map((item, index) => <DrawerItem label={item.title} focused onPress={() => drawerNavigate(navigation, item)} key={index}/>)}
and it works fine but i cannot select active item from them...
help me please how right to add items to drawer and navigate always to one component with different params

React Native - Header Title on Material Bottom Tabs on Navigation 5

I have a Material Bottom Tabs into a Stack Navigator in Navigation 5.
It is suppose dto get the title and route it throught a function but doesn't work.
function:
function getHeaderTitle(route) {
const routeName = route.state
? route.state.routes[route.state.index].name
: route.params?.screen || 'Home';
switch (routeName) {
case 'Favorites':
return 'Favorites';
case 'Info':
return 'Info';
}
}
materialBottomTabs
const NavTab = () => {
return (
<Tab.Navigator>
<Tab.Screen
name="Favorites"
component={FavoritesScreen}
options={({route}) => ({
tabBarLabel: 'Fav',
tabBarIcon: () => IconFav(),
})}
/>
<Tab.Screen
name="Info"
component={InfoScreen}
options={({route}) => ({
headerTitle: 'Info',
tabBarLabel: 'Info',
tabBarIcon: () => IconInfo(),
})}
/>
</Tab.Navigator>
stackNavigator
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen
name="Home"
component={NavTab}
options={({route}) => ({
headerTitle: getHeaderTitle(route),
})}
/>
<Stack.Screen
name="Detail"
component={ItemDetailScreen}
/>
</Stack.Navigator>
</NavigationContainer>;
I easly set the title of a screen listed in the Stack.
Example:
const ItemDetailScreen = ({route, navigation}) => {
useEffect(() => {
navigation.setOptions({
headerTitle: item.title,
});
}, []);
But I can't set the title of the screens into the material bottom tabs. They always have the name set into the stack navigator. How can I do it?
options={({route}) => ({
headerTitle: getHeaderTitle(route),
})}
i think it should be:
options={({route}) => ({
title: getHeaderTitle(route),
})}