7
votes

(also asked in https://github.com/react-navigation/react-navigation/issues/4059#issuecomment-453100740)

I have replaced a static TabNavigator with a dynamic one, and things seem to work.
However, props that have been passed as expected are not passed any more the same way.
Any idea how to fix this? Either by having the props passed as in the static solution, or by passing the required props (this.props.navigation).

This is my top navigator:

export default createDrawerNavigator({
    Drawer: MainDrawerNavigator,
    Main: MainTabNavigator
}, {
  contentComponent: props => <Drawer {...props} />,
});

This the static Tab Navigator and one of the Stacks:

const ProfileStack = createStackNavigator({
  Profile: {
    screen: Profile,
    navigationOptions: () => ({
        title: 'Profile'
    })
  }
}, {
  initialRouteName: 'Profile'
});

ProfileStack.navigationOptions = {
  tabBarLabel: 'Profile',
  tabBarIcon: ({ focused }) => (
    <TabBarIcon
      focused={focused}
      name= 'md-person' />
  )};

const MainTabNavigator = createBottomTabNavigator({
  RequestStack,
  ProfileStack
}, {
    headerMode: 'none',
    initialRouteName: ProfileStack
});

And the Profile screen:

import React from 'react';
import { View, TouchableOpacity } from 'react-native';
import { Container, Header, Content, Text
} from 'native-base';

export default class Profile extends React.Component {
  static navigationOptions = {
    header: null
  };

  constructor(props) {
    super(props);
  }

  render() {
    console.log('in Profile. this.props:');
    console.log(this.props);
    return (
        <Container style={styles.container}>
          <Header>
              <TouchableOpacity
                onPress={() => this.props.navigation.openDrawer()}>
                 <Icon name="md-more" />
              </TouchableOpacity>
          </Header>
          <Content>

           <Text>aaa</Text>
          </Content>
        </Container>
    );
  }
}

A click on the "md-more" icon opens the drawer (this.props.navigation.openDrawer).

Using a dynamic tab navigator - openDrawer is not passed any more to 'Profile'.

when I replace the above static Tab Navigator with the following Dynamic one, this.props.navigation.openDrawer is not passed and therefore not defined in 'Profile' (Profile doesn't change, the change is only in the bottom tab navigator).

Here is the dynamic Tab Navigator:

export default class DynamicTabNavigator extends React.Component {

  constructor(props) {
    super(props);
  }

  _tabNavigator() {
    let tabs = {};
    const a = 2; 
    if (a > 1) {   // the actual line is obviously different, I am trying to simplify the example
      tabs = { RequestStack, ManageStack, MessagesStack, ProfileStack };
    } else {
      tabs = { WorkStack, ManageStack, MessagesStack, ProfileStack };
    }

    console.log('in _tabNavigator. this.props.navigation:');
    console.log(this.props.navigation);
    return createBottomTabNavigator(tabs, {
        headerMode: 'none',
    });
  }

  render() {
    const Tabs = this._tabNavigator.bind(this)();
    return (
      <Tabs/>
    );
  }
}

This is the output of console.log() from DynamicTabNavigator:

in _tabNavigator. this.props.navigation:
 Object {
   "actions": Object {
     "closeDrawer": [Function closeDrawer],
     "goBack": [Function goBack],
     "navigate": [Function navigate],
     "openDrawer": [Function openDrawer],
     "setParams": [Function setParams],
     "toggleDrawer": [Function toggleDrawer],
   },
   "addListener": [Function addListener],
   "closeDrawer": [Function anonymous],
   "dangerouslyGetParent": [Function anonymous],
   "dispatch": [Function anonymous],
   "getChildNavigation": [Function getChildNavigation],
   "getParam": [Function anonymous],
   "getScreenProps": [Function anonymous],
   "goBack": [Function anonymous],
   "isFocused": [Function isFocused],
   "navigate": [Function anonymous],
   "openDrawer": [Function anonymous],
   "router": undefined,
   "setParams": [Function anonymous],
   "state": Object {
     "key": "Main",
     "params": undefined,
     "routeName": "Main",
   },
   "toggleDrawer": [Function anonymous],
 }

This is the output of console.log() from Profile, when DynamicTabNavigator is in place:

(I expected all props, like for instance, openDrawer, to be the same as for DynamicTabNavigator, and I don't understand why they aren't)

 in Profile. this.props:
 Object {
   "appMode": "WORK_MODE",
   "dispatch": [Function anonymous],
   "navigation": Object {
     "actions": Object {
       "dismiss": [Function dismiss],
       "goBack": [Function goBack],
       "navigate": [Function navigate],
       "pop": [Function pop],
       "popToTop": [Function popToTop],
       "push": [Function push],
       "replace": [Function replace],
       "reset": [Function reset],
       "setParams": [Function setParams],
     },
     "addListener": [Function addListener],
     "dangerouslyGetParent": [Function anonymous],
     "dismiss": [Function anonymous],
     "dispatch": [Function anonymous],
     "getChildNavigation": [Function getChildNavigation],
     "getParam": [Function anonymous],
     "getScreenProps": [Function anonymous],
     "goBack": [Function anonymous],
     "isFocused": [Function isFocused],
     "navigate": [Function anonymous],
     "pop": [Function anonymous],
     "popToTop": [Function anonymous],
     "push": [Function anonymous],
     "replace": [Function anonymous],
     "reset": [Function anonymous],
     "router": undefined,
     "setParams": [Function anonymous],
     "state": Object {
       "key": "id-1547113035295-8",
       "routeName": "Profile",
     },
   },
   "screenProps": undefined,
 }

Questions regarding @dentemm's solution:

I a not sure how to implement your solution...

  • Lets say that I have in my TabRoutes the three screens that you specified in your example
  • In my redux state I have a variable called 'appState'. If it is true I want to display first & second, if false first & third.
  • Here is the code that I wrote based on your example. I am not sure, however, which component is included in CustomTabBar. Can you elaborate?

    import React from 'react';
    class CustomTabBar extends React.Component {
    
    render() {
    // a tab bar component has a routes object in the navigation state
    const { navigation } = this.props;
    
      // appState is extracted from redux state, see below
      if (this.props.appState) {
        return (
          <View>
            <???
              name='First'
              onPress={this.navigationHandler}
              focused={navigation.state.index === index}
            />
            <???
              name='Second'
              onPress={this.navigationHandler}
              focused={navigation.state.index === index}
            />
          </View>
        );
      } else {
        return (
          <View>
            <???
              name='First'
              onPress={this.navigationHandler}
              focused={navigation.state.index === index}
            />
            <???
              name='Third'
              onPress={this.navigationHandler}
              focused={navigation.state.index === index}
            />
          </View>
          );
        }
      }
    
      navigationHandler = (name) => {
        const {navigation} = this.props;
        navigation.navigate(name);
      }
    }
    
    const mapStateToProps = state => {
     const { appState } = state.app;
     return { appState };
    };
    
    export default connect(mapStateToProps)(CustomTabBar);
    
2
this.props.navigation is undefined ? try passing it in the component <Tabs navigation={this.props.navigation}/>ValdaXD
It is not undefined. I added the console.log() output for both modules. Adding what you suggested causes the app to abort with the following message: TypeError: TypeError: TypeError: No "routes" found in navigation state. Did you try to pass the navigation prop of a React component to a Navigator child? See reactnavigation.org/docs/en/…Yossi
Just a question: do you really need a dynamic TabNavigator, or would a dynamic TabBar suffice?dentemm
@dentemm What is a dynamic TabBar? I need to dynamically set which tabs are displayed, based on redux state variableYossi
You could easily create a custom TabBar component, and connect that to redux. I will create an answer.dentemm

2 Answers

5
votes

You can also leave the TabNavigator as is, and create a custom TabBar component with custom TabBarItem components. You can connect that custom TabBar to your redux state, and hide/display the custom TabBarItems according your needs.

And then you simply add all possible routes to the TabNavigator as you would always do.

Routes

const TabRoutes = createBottomTabNavigator({
  First: {screen: SomeScreen},
  Second: {screen: SomeStack},
  Third: {screen: AnotherStack}
},{
  initialRouteName: 'First',
  tabBarComponent: CustomTabBar
});

CustomTabBar

Some basic example on how you could hide the tab bar items, so obviously this needs to be adjusted according your own requirements

import CustomTabBarItem from '...'  ; 

class CustomTabBar extends React.Component {

  render() {

    // a tab bar component has a routes object in the navigation state
    const {navigation, appState} = this.props;
    const routes = navigation.state.routes;

    return (
      <View style={styles.container}>
        // You map over all existing routes defined in TabNavigator
        {routes.map((route, index) => {

              // This could be improved, but it's just to show a possible solution
              if (appState && route.routeName === 'x') {
                return <View/>;
              } else if (!appState && route.routeName === 'y') {
                return <View/>;
              }

              return (<CustomTabBarIcon
                key={route.key}
                name={route.routeName}
                onPress={this.navigationHandler}
                focused={navigation.state.index === index}
                appState={appState}
              />);
        })}
      </View>
    );
  }

  navigationHandler = (name) => {

    const {navigation} = this.props;
    navigation.navigate(name);
  }
}

const styles = StyleSheet.create({
  container: {
    width: '100%',
    flexDirection: 'row'
  }
})

const mapStateToProps = (state) => {
  return {
    appState: state.app.appState // boolean
  };
};

export default connect(mapStateToProps)(CustomTabBar);

CustomTabBarItem

class CustomTabBarItem extends React.PureComponent {
  render() {

    const {name, focused} = this.props;

    return (
      <View style={styles.tabItem}>
        // Some icon maybe
        <Text style={/*different style for focused / unfocused tab*/}>{name}</Text>
      </View>
    );
  }
}

const styles = StyleSheet.create({
  tabItem: {
    flex: 1
  }
})
1
votes

Ok after looking at your code I see what's the problem... the problem is.. you are not nesting your navigation prop the right way. When you nest A normal tabnavigator in a drawernavigator it works fine, because you are not rendering anything , you are doing it the react navigation way.

But.. when you try to use a dynamic tabnavigator, you are not returning a tabnavigator to the drawernavigator, you are returning a COMPONENT export default class DynamicTabNavigator extends React.Component {}, and inside his render function you have a tabnavigator...

So.. you have 2 possible solutions...

The first one being use just a function and call it without having a custom component

export function tabNavigator() {
    let tabs = {};
    const a = 2; 
    if (a > 1) {   // the actual line is obviously different, I am trying to simplify the example
      tabs = { RequestStack, ManageStack, MessagesStack, ProfileStack };
    } else {
      tabs = { WorkStack, ManageStack, MessagesStack, ProfileStack };
    }

   // console.log('in _tabNavigator. this.props.navigation:');
    //console.log(navigation);
    return createBottomTabNavigator(tabs, {
        headerMode: 'none',
    });
}

and in your root navigator

import {tabNavigator} from './TabNavigator'
    const Tabs = tabNavigator()
    export default createDrawerNavigator({
        Drawer: MainDrawerNavigator,
        Main: Tabs
    }, {
      contentComponent: props => <Drawer {...props} />,
    });

Dont' know if that is gonna work

Second solution

Manually pass the navigation prop to all the screens of the dynamic tabnavigator, it's really ugly, but it's a workaround when you put a navigator inside a component

_tabNavigator() {
    let tabs = {};
    const a = 2; 
    if (a > 1) {   // the actual line is obviously different, I am trying to simplify the example
      tabs = { RequestStack: {screen:<RequestStack navigation={this.props.navigation}/>, navigationOptions: () => ({
    tabBarLabel:'Request',
    tabBarIcon:<YourComponent/>})}, 
               ManageStack:{screen:<ManageStack navigation={this.props.navigation}/>}, MessagesStack:{screen:<MessagesStack navigation={this.props.navigation}/>},
               ProfileStack:{screen:<ProfileStack navigation={this.props.navigation}/>}};
    } else {
      tabs = { WorkStack, ManageStack, MessagesStack, ProfileStack }; //repeat
    }

    console.log('in _tabNavigator. this.props.navigation:');
    console.log(this.props.navigation);
    return createBottomTabNavigator(tabs, {
        headerMode: 'none',
    });

Pro tip: don't do dynamic navigation inside a component, the navigation prop will be lost if you don't pass it manually

EDIT N2:

const ProfileStack = ({props}) => createStackNavigator({
  Profile: {
    screen: <Profile navigation={props.navigation}/>,
    navigationOptions: () => ({
        title: 'Profile'
    })
  }
}, {
  initialRouteName: 'Profile'
});