Nested TabNavigator inside StackNavigator: controlling the header - reactjs

I have a setup much like this:
let Tabs = createBottomTabNavigator({
screen1: Screen1,
screen2: Screen2
})
let Stack = createStackNavigator({
tabs: Tabs
otherScreen: OtherScreen
})
The stack navigator has a header, and that's fine. What I want is to get different header icons depending on what tab I'm currently on.
I'm using the following versions:
"react": "16.3.1",
"react-native": "~0.55.2",
"react-navigation": "^2.2.5"
I've considered switching up my setup so that each tab screen has its own StackNavigator but I like having the sliding animation when switching tabs and I don't want the header icons to slide along. The header bar should remain static but simply show different icons depending on the current tab.

You can use like this below, https://reactnavigation.org/docs/en/stack-navigator.html
//Screen1 Stack.
const Screen1 = createStackNavigator ({
Home: {
screen: Home,
navigationOptions: {
header: null //Need to set header as null.
}
}
});
//Screen2 Stack
const Screen2 = createStackNavigator ({
Profile: {
screen: Profile,
navigationOptions: {
header: null //Need to set header as null.
}
}
});
let Tabs = createMaterialTopTabNavigator({
Screen1:{
screen: Screen1 //Calling Screen1 Stack.
},
Screen2:{
screen: Screen2 //Calling Screen2 Stack.
}
},{ tabBarPosition: 'bottom' }) //this will set the TabBar at Bottom of your screen.
let Stack = createStackNavigator({
tabs:{
screen: Tabs, //You can add the NavigationOption here with navigation as parameter using destructuring.
navigationOptions: ({navigation})=>{
//title: (navigation.state.routes[navigation.state.index])["routeName"]
//this will fetch the routeName of Tabs in TabNavigation. If you set the routename of the TabNavigation as your Header.
//use the following title property,this will fetch the current stack's routeName which will be set as your header in the TabBar.
//title: (navigation.state.routes[navigation.state.index]["routes"])[(navigation.state.routes[navigation.state.index]["index"])].routeName
//you can use switch case,on matching the route name you can set title of the header that you want and also header left and right icons similarly.
switch ((navigation.state.routes[navigation.state.index]["routes"])[(navigation.state.routes[navigation.state.index]["index"])].routeName) {
case "Screen1":
return {
title: "Home",
headerLeft: (<Button
onPress={()=> alert("hi")}
title="Back"
color="#841584"
accessibilityLabel="Learn more about this purple button"
/> ),
headerRight: <Button title= "Right"/>
}
case "Screen2":
return {
title: "Profile",
headerLeft: (<Button
onPress={()=> alert("hi")}
title="Back"
color="#841584"
accessibilityLabel="Learn more about this purple button"
/> ),
headerRight: <Button title= "Right"/>
}
default:
return { title: (navigation.state.routes[navigation.state.index]["routes"])[(navigation.state.routes[navigation.state.index]["index"])].routeName }
}
}
},
otherScreen:{
screen: OtherScreen
}
})
//navigationOptions
navigationOptions: ({navigation})=>{
//title: (navigation.state.routes[navigation.state.index])["routeName"]
//this will fetch the routeName of Tabs in TabNavigation. If you set the routename of the TabNavigation as your Header.
//use the following title property,this will fetch the current stack's routeName which will be set as your header in the TabBar.
//title: (navigation.state.routes[navigation.state.index]["routes"])[(navigation.state.routes[navigation.state.index]["index"])].routeName
switch ((navigation.state.routes[navigation.state.index]["routes"])[(navigation.state.routes[navigation.state.index]["index"])].routeName) {
case "Screen1":
return {
title: "Home",
headerLeft: (<Button
onPress={()=> alert("hi")} //Here you can able to set the back behaviour.
title="Back"
color="#841584"
accessibilityLabel="Learn more about this purple button"
/> ),
headerRight: <Button title= "Right"/>
}
case "Screen2":
return {
title: "Profile",
headerLeft: (<Button
onPress={()=> alert("hi")}
title="Back"
color="#841584"
accessibilityLabel="Learn more about this purple button"
/> ),
headerRight: <Button title= "Right"/>
}
default:
return { title: (navigation.state.routes[navigation.state.index]["routes"])[(navigation.state.routes[navigation.state.index]["index"])].routeName }
}
}
//alert(navigation.state)
{
"routes":[
{
"key":"Screen1",
"routeName":"Screen1",
"routes":[
{
"key":"Home",
"routeName":"Home",
}
],
"index":0,
"isTransitioning":false,
"key":"id-1530276062643-0"
},
{
"key":"Screen2",
"routeName":"Screen2",
"routes":[
{
"key":"Profile",
"routeName":"Profile",
}
],
"index":0,
"isTransitioning":false,
"key":"id-1530276062643-0"
}
],
"index":0,
"isTransitioning":false,
"routeName":"tabs",
"key":"id-1530276062643-0"
}
//(navigation.state.routes[navigation.state.index])["routeName"]
//(navigation.state.routes[navigation.state.index]["routes"])[(navigation.state.routes[navigation.state.index]["index"])].routeName
this will give the current route name of the tab inside StackNavigation.
The above code will set the title in root stack header where the TabBar resides as first route,so we are setting the header as null for the individual stack in TabBar. By using this way will provide Animation while switching the screens in TabBar since header will remain static.
you can find the working copy here https://www.dropbox.com/s/jca6ssn9zkzh9kn/Archive.zip?dl=0
Download this and Execute the following.
npm install //to get dependencies
react-native upgrade //to get android and ios folder
react-native link //to link dependencies and libraries
react-native run-ios (or) react-native run-android
you can use the above, Let me know if any.

You can achieve the desired behavior with the current navigation stack configuration of yours. You might need to change couple of things and combine couple of properties but it is fairly simple when you get yor head around it.
I try to explain it with a small example.
Consider having below navigators;
const Tabs = createBottomTabNavigator({
screen1: Tab1,
screen2: Tab2
})
const Stack = createStackNavigator({
tabs: {
screen: TabsPage,
navigationOptions: ({navigation}) => {
return { title: (navigation.state.params && navigation.state.params.title ? navigation.state.params.title : 'No Title' ) }
}
},
otherScreen: Page
})
As you can see I am setting the title parameter from the navigation state. To be ale to set the param for that navigator, we are going to get help from screenProps property;
class TabsPage extends Component {
onTabsChange = (title) => {
this.props.navigation.setParams({ title })
}
render() {
return(<Tabs screenProps={{ onTabsChange: this.onTabsChange }} />)
}
}
I created a wrapper component for tab navigator and passed down a function which is setting the title parameter.
For the last part we need to know how and when to use that function we passed down. For that we are going to use addListener navigation prop
class Tab1 extends React.Component {
setTitle = () => {
this.props.screenProps.onTabsChange('Title from Tab 1')
}
componentDidMount() {
this.props.navigation.addListener('willFocus', this.setTitle)
}
render() {
return <View><Text>{'Tab1'}</Text></View>
}
}
When our tab focused the passed function will run and then set the title for that tab. You can use this process for setting different buttons or icons for the header. You can find the working snack here.

const RootStack = createStackNavigator(
{
Home: {screen: HomeScreen},
FilterScreen: createMaterialTopTabNavigator({
Tab1: {screen:Tab1Screen},
Tab2: {screen: Tab2Screen},
}),
},
{
mode: 'modal',
headerMode: 'none',
}
);
render() {
return <RootStack/>;
}

In AppNavigation.js //or where you have defined your routes.
let Tabs = createBottomTabNavigator({
screen1: Screen1,
screen2: Screen2
})
let Stack = createStackNavigator({
tabs: Tabs
otherScreen: OtherScreen
},{
headerMode:"float", //Render a single header that stays at the top and animates as screens are changed. This is a common pattern on iOS.
headerTransitionPreset:"fade-in-place" //this will give a slight transition when header icon change.
}
)
In Screen1.js
class Screen1 extends Component {
static navigationOptions = ({ navigation }) => {
return {
...
headerLeft: <HeaderLeftImage navigation={navigation} image={"Image_For_Screen1"}/>,
...
}
}
...
}
NOTE: You can add title, headersStyle,headerRight in same way read this link header configuration for more detail
In Screen2.js do similar to Screen1
class Screen2 extends Component {
static navigationOptions = ({ navigation }) => {
return {
...
headerLeft: <HeaderLeftImage navigation={navigation} image={"Image_For_Screen2"}/>,
...
}
}
...
}
Header.js
export const HeaderLeftImage = (props) => (
<View style={{
'add styles'
}}>
<TouchableOpacity onPress={() => {"Add action here" }}>
<Image source={props.image} resizeMethod='resize' style={{ height: 30, width: 90, resizeMode: 'contain' }} />
</TouchableOpacity>
</View>
)
Hope this helps, if you have any doubt regarding the code feel free to ask.

If you are using react navigation <2 i.e ~1.5.* You can set it like this.
const Tabs = TabNavigator({
Tab1:{
screen: Tab1,
navigationOptions: ({navigation}) => {
return { title: "Tab 1 Heading", tabBarLabel:"Tab 1 "}
},
}
Tab2:{
screen: Tab2
navigationOptions: ({navigation}) => {
return { title: "Tab 2 Heading", tabBarLabel:"Tab 2 "}
}
}
})
const Stack = StackNavigator({
tabs: {
screen: Tabs
navigationOptions: ({navigation}) => {
return { title: "Stack"}
}
},
otherScreen: Page
})
I'm not sure why did they remove this feature, when you try the same it won't work on the latest react-navigation. Now title object key is used for fallback it seems.
This may be helpful some users. If you wish you can try downgrading also.
I have the Upgraded the React Navigation for my project I think this way will be useful to someone
const Tabs = TabNavigator({
Tab1:{
screen: Tab1,
navigationOptions: ({navigation}) => {
return { tabBarLabel:"Tab 1 "}
}},
Tab2:{
screen: Tab2
navigationOptions: ({navigation}) => {
return { tabBarLabel:"Tab 2 "}
}}
});
Tabs.navigationOptions = ({navigation})=>{
const { routeName } = navigation.state.routes[navigation.state.index]; //This gives current route
switch(routeName){
case "Tab1":
headerTitle="Tab 1";
break;
case "Tab1":
headerTitle="Tab 1";
break;
}
return {
headerTitle: headerTitle
}
}

You can simply use property headerShown: false. I have created a working example of react-navigation version: 5.x.x, nesting different navigators: stack, drawer & bottom-tabs.
Here's the link to github:
https://github.com/mvpbuddy/react-native-nested-navigators/
For a more detailed overview, you can checkout my blog:
https://mvpbuddy.io/blog/detail/how-to-build-an-app-with-nested-stack-drawer-bottom-tab-navigators

Related

React Navigation 4 MaterialTopTabNavigator - activeLabelStyle

I am creating a MaterialTopBar with React Navigation v4. I am facing an issue where i am not able to style to active state of the label - or even the tab.
The docs state that you can supply a function which renders a custom label. Here you could supply a custom style based on the focused parameter. This is what i am doing, but it seems like it just ignores my custom label function. Below is my code.
export default createMaterialTopTabNavigator(
{
Drinks: {
screen: PickupDrinksScreen,
},
Food: {
screen: PickupFoodScreen,
},
Snacks: {
screen: PickupSnacksScreen,
}
},
{
navigationOptions: ({ navigation }) => {
return {
tabBarLabel: ({ focused }) => {
const { routeName } = navigation.state;
return <Text style={[styles.label, focused && styles.focusedLabel]}>{routeName}</Text>;
}
};
}
})
Thanks in advance!
I managed to achieve this inside a BottomTabNavigator but I think that it does not really differ for your needs. Check out my code:
Secure: {
screen: SecureStack,
navigationOptions: {
tabBarIcon: ({ focused }) => {
if (focused) {
return <Ionicons name="md-unlock" size={24} color="#ccc" />;
} else {
return <Ionicons name="md-lock" size={24} color="#ccc" />;
}
},
title: "Secure"
}
}
try it like I did, I pass the navigationOptions right to the screen instead of passing it to all tabs. ~ Faded

Main Navigation 3 (Drawer and Stack) together problem

I am using react navigation version 3
I trying to use Drawer and Stack together
I am implementing a navigation drawer and works well
But when I use stack navigation I can't see the back button option in the header
Drawer -> ListScreen ->onpress item open stack navigation to view profile ->
here I can't go back to ListScreen
I tried edit navigation option and add custom properties but still, I don't understand why it's not visible
const AuthStackNavigation = createStackNavigator({
HomeStack: {
screen.
screen: LeadScreen,
headerBackTitleVisible:true,
navigationOptions: ({ navigation }) => ({
headerLeft: <Icon
name='arrow-back' onPress={ () => { navigation.goBack() }} style={{width:100}} />
,
title: `${navigation.state.params.name}'s Profile'`,
headerStyle: {
backgroundColor: '#f4511e',
},
headerTitleStyle: { flex: 1, textAlign: 'center'},
}),
},
});
//navigate('Profile', { name: 'Brent' })
const AppStackNavigator = createDrawerNavigator({
'בית':HomeScreen,
'רשימת לידים':LeadsScreen,
'ניהול סוכנים':ManageUsersScreen
,
'הגדרות':SettingsScreen
},{
contentComponent:CustomerDrawerComponent
}
)
const MainNavigation = createSwitchNavigator({
HomeDrawer: AppStackNavigator,
AuthStack: AuthStackNavigation, // You will use this.props.navigation.replace('HomeDrawer') after login process.
})
export default createAppContainer(MainNavigation);

How to have a unique icon for two screens with react navigation

I have a bottom tab navigator in react native and I put my screens into it this way :
const AppStack = createBottomTabNavigator(
{
FirstPage : {
screen: FirstPage,
navigationOptions: {
tabBarVisible: true,
}
},
SecondPage : {
screen: SecondPage,
navigationOptions: {
tabBarVisible: true,
tabBarButtonComponent: () => false
}
},
ThirdPage : {
screen: ThirdPage,
navigationOptions: {
tabBarVisible: true,
}
},
},
{
defaultNavigationOptions: ({navigation}) => ({
tabBarIcon: ({focused}) => {
if (navigation.state.routeName === 'FirstPage' || navigation.state.routeName === 'SecondPage') {
icon = focused ? require('iconPathFocused.png') : require('iconPathNotFocused.png)
} else if (navigation.state.routeName === 'ThirdPage') {
[...]
}
return <TabIcon path={icon}/>
}
})
}
)
The problem is that when I'm on the SecondPage screen, the tabbar is still visible but none of the icons are "hilighted" because not focused.
The thing is I don't want any particular icon to be displayed for the second page. I want it to be like a child of the First page so when I navigate from the First page to the Second one, the same icon (still FirstPage's one so) is displayed and highlighted.
Thank you very much !
You can set navigationOptions something like this with tabBarIcon for all your screens
So you can set different focused and not focused icons for each and every tab.
FirstPage : {
screen: FirstPage,
navigationOptions: {
tabBarLabel: "First Page",
tabBarIcon: ({ tintColor, focused }) => (
<Image source={focused ? require('iconPathFocused.png') : require('iconPathNotFocused.png')} style={{height: 28, width: 28}}/>
),
}
},
SecondPage : {
screen: SecondPage,
navigationOptions: {
tabBarLabel: "Second Page",
tabBarIcon: ({ tintColor, focused }) => (
<Image source={focused ? require('iconPathFocused.png') : require('iconPathNotFocused.png')} style={{height: 28, width: 28}}/>
),
}
},
Add this to bellow defaultNavigationOptions
tabBarOptions: {
activeTintColor: 'tomato',
inactiveTintColor: 'gray',
}
this might help.
For more details have help from React-navigation official page : https://reactnavigation.org/docs/en/tab-based-navigation.html
Check customise appearance there that might help further.

double header stack navigation react-native

I am using a StackNavigator in react native app.
The problem is that in my app, it creates two headers ...
I would like to keep the upper one to go back from the screen. Is it possible without recreate the back button manually ?
Screen:
class CommandsList extends React.Component {
constructor(props){
super(props);
}
addCommand = () => {
this.props.navigation.navigate("CreateCommand");
}
render() {
return (
<SafeAreaView style={{flex:1}}>
<MyList itemsUrl="http://localhost:9000/commands"/>
<Button title="Ajouter" onPress={this.addCommand}></Button>
</SafeAreaView>
);
}
}
export default StackNavigator({
CommandsList : {
screen : CommandsList,
},
});
EDIT :
App.js
const navigationOptions = ({ navigation }) => ({headerLeft: <Icon name {'chevron-left'} onPress={ () => { navigation.goBack() }} />})
const RootStack = StackNavigator(
{
CommandsList: {
screen: CommandsList,
},
CreateCommand: {
screen: CreateCommand,
},
ListFournisseurs: {
screen: ListFournisseurs,
},
ListAffaires: {
screen: ListAffaires,
}
},
{
initialRouteName: 'CommandsList',
headerMode:'none',
navigationOptions:{navigationOptions}
}
);
According to the docs, if you want to disable the header of the StackNavigator, you can apply the config at your StackNavigatorConfig as headerMode: 'none'. It is back propagated from Child to Parent, if Parent is none then Child will also not be rendered.
Therefore for a single header, in your case you should do
export default StackNavigator({
CommandsList : {
screen : CommandsList,
},
}, {
headerMode: 'none'
});
For the back button in the Parent Stack, you can create a component as
const navigationOptions = ({ navigation }) => ({
headerLeft: <Icon name={'arrow-left'} //<== Vector Icon Here
onPress={ () => { navigation.goBack() }} />
const RootStack = StackNavigator(
RouteConfigs,
//... StackNavigatorConfigs,
navigationOptions
)

How to get title of screen in custom header component?

Hope it's ok.
I Can't understand how custom header it's supposed to work.
I just want to send static properties to my Header depending on which route is selected.
I have:
export default AppNavigator = StackNavigator({
Index:{
screen: BottomNavigator,
}
},{
navigationOptions: {
header: AppHeader,
},
headerMode:'float',
})
And my BottomNavigator is:
const BottomNavigator = TabNavigator({
TabMenu1: {
screen: () => <Text> Resumé </Text>,
navigationOptions: {
title: 'Resumé'
}
},
TabMenu2: {
screen: () => <Text> Sells </Text>,
navigationOptions: {
title: 'Sells'
}
},
},{
tabBarComponent: BottomNavigation
});
I'm expecting {props.title} in my custom header, is that right?
Additional info: My full route stack is:
AuthNavigator have a wrapper that is connected to redux and have:
const AuthNavigator = StackNavigator({
SignedIn: {
screen: MainNavigator
}
},{
headerMode:'none',
initialRouteName: 'SignedIn'
});
MainNavigator:
const MainNavigator = StackNavigator({
Drawer: {
screen: DrawerNav
},
}, {
headerMode:'none',
});
DrawerNav:
const DrawerNav = DrawerNavigator({
Menu1: {
screen: AppNavigator
},
}, {
contentComponent: DrawerNavigation,
drawerWidth: 300
});
AppNavigator and BottomNavigator are described above
for React Navigation 5 this works
props.scene.descriptor.options.title
You can specify title like this :
RouteName:
{
screen: RouteScreen,
navigationOptions: ({navigation}) => ({
title: navigation.state.params.title || 'StaticTitle'
})
}
Where navigation.state.params.title is the title you send as a params when you navigate (so if you want to add a variable for exemple). If you just want to use a static title, just use title: 'StaticTitle'
export const Navigator = StackNavigator(routes, {
headerMode: 'screen',
navigationOptions: ({ navigation }) => ({
header: props => <Header {...props} />,
}),
})
I did find a work around but it seems pretty verbose for what seems to be a common use case..
// Header.js
...
render() {
const { getScreenDetails, scene } = this.props
const details = getScreenDetails(scene)
return (
<View>
<Text>{details.options.title}</Text>
<View>
)
Resolved with a code extracted of Header.js file of react-navigation:
The right way to get the title inside a custom header is:
const sceneOptions = props.getScreenDetails(props.scene).options;
const sceneTitle = sceneOptions.title;
Thanks all!

Resources