How can I render a Tab Item for React Navigation Material Bottom Tab based on store value?

I'm using React Navigation Material Bottom Tabs and I need to add an option based on a store value. I have a Material Bottom Tab for Authenticated user, then I wanna get the user kind value from store and render a tab item based on value at my reducer.

I tried the below solution but I always get store initial state.

createMaterialBottomTabNavigator(
  {
    Events: {
      screen: EventsStack,
      navigationOptions: {
        tabBarLabel: 'Agenda',
        tabBarIcon: ({ tintColor }) => (
          <MaterialCommunityIcons name="calendar" size={ICON_SIZE} color={tintColor} />
        ),
      },
    },
    ...((() => {
      const userKind = store.getState() && store.getState().auth.user.kind;

      return userKind === 'p';
    })() && {
      Consultations: {
        screen: PatientsStack,
        navigationOptions: {
          tabBarLabel: 'Atendimentos',
          tabBarIcon: ({ tintColor }) => (
            <MaterialCommunityIcons name="doctor" size={ICON_SIZE} color={tintColor} />
          ),
        },
      },
    }),
    Patients: {
      screen: PatientsStack,
      navigationOptions: {
        tabBarLabel: 'Pacientes',
        tabBarIcon: ({ tintColor }) => (
          <MaterialCommunityIcons name="account-multiple" size={ICON_SIZE} color={tintColor} />
        ),
      },
    },
    Settings: {
      screen: SettingsStack,
      navigationOptions: {
        tabBarLabel: 'Ajustes',
        tabBarIcon: ({ tintColor }) => (
          <MaterialCommunityIcons name="settings" size={ICON_SIZE} color={tintColor} />
        ),
      },
    },
  },
  {
    initialRouteName: 'Events',
    labeled: false,
    shifting: false,
    activeTintColor: Colors.dsBlue,
    barStyle: {
      borderTopWidth: 1,
      borderTopColor: Colors.dsSky,
      backgroundColor: Colors.colorWhite,
    },
    tabBarOptions: {
      activeTintColor: Colors.dsBlue,
      activeBackgroundColor: Colors.dsSkyLight,
      inactiveTintColor: Colors.dsInkLighter,
      inactiveBackgroundColor: Colors.dsSkyLight,
      upperCaseLabel: false,
      labelStyle: {
        fontSize: 11,
      },
      style: {
        backgroundColor: Colors.dsSkyLight,
      },
      showIcon: true,
      pressColor: Colors.dsSkyLight,
      indicatorStyle: {
        backgroundColor: Colors.dsBlue,
      },
    },
  },
)

I want to conditionally render Consultations tab item based on my store value.

Upvotes: 1

Views: 1181

Answers (1)

remeus
remeus

Reputation: 2449

Using store.getState() in the navigator will give you the initial store but as you have pointed out, it will not get called again when the store is updated, so the navigator will never change.

The solution to have the navigation updated when the state changes is to use a component that is connected to the Redux store.

Let's say you want to change the title of a tab depending on a value in your Redux store.

In that case, you would simply define a Label.js component like so:

import React from 'react';
import { connect } from 'react-redux';
import { Text } from 'react-native';


const Label = ({ user }) => {
  if (!user) return 'Default label';
  return <Text>{user.kind}</Text>;
};


function mapStateToProps(state) {
  return { user: state.auth.user };
}

export default connect(mapStateToProps)(Label);

Here we assume that you have a reducer that updates the auth key in your store with the user object whenever it changes.

Now all you would have to do is import <Label /> in your navigation:

import Label from './Label';

export default createMaterialBottomTabNavigator({
  Events: {
    screen: EventsStack,
    navigationOptions: {
      tabBarLabel: <Label />,
    },
  },
});

Voilà! Whenever you update auth.user.kind in your store, the tab navigation gets updated.


Now I am aware that in your case you want something a bit more complicated than updating the label depending on the store, you want to display or hide a tab dynamically.

Unfortunately react-navigation does not provide a hideTab option for a given tab yet. There is a tabBarVisible option but it only applies to the whole bar, not a single tab.

With the labels, we managed to do it by connecting a component to the store. But what component should we target here?

The workaround is to use the tabBarComponent option. It allows us to override the component to use as the tab bar. So we just have to override it with a component that is connected to the store and we have our dynamic tab bar!

Now our createMaterialBottomTabNavigator becomes:

import WrappingTabBar from './WrappingTabBar';

export default createMaterialBottomTabNavigator({
  Events: {
    screen: EventsStack,
    navigationOptions: {
      tabBarLabel: 'Agenda',
    },
  },
  Consultations: {
    screen: PatientsStack,
    navigationOptions: {
      tabBarLabel: 'Atendimentos',
    },
  },
}, {
  tabBarComponent: props => <WrappingTabBar {...props} />, // Here we override the tab bar component
});

And we define <WrappingTabBar> in WrappingTabBar.js by rendering a basic BottomTabBar connected to the store and that filters out the routes in the navigation state that we do not want.

import React from 'react';
import { connect } from 'react-redux';
import { BottomTabBar } from 'react-navigation';

const TabBarComponent = props => <BottomTabBar {...props} />; // Default bottom tab bar

const WrappingTabBar = ({ navigation, user, ...otherProps }) => (
  <TabBarComponent
    {...otherProps}
    navigation={{
      ...navigation,
      state: {
        ...navigation.state,
        routes: navigation.state.routes.filter(r => r.routeName !== 'Consultations' || (user && user.kind === 'p')), // We remove unwanted routes
      },
    }}
  />
);


function mapStateToProps(state) {
  return { user: state.auth.user };
}

export default connect(mapStateToProps)(WrappingTabBar);

This time, whenever your auth reducer receives a new value for auth.user, it updates the store. Since WrappingTabBar is connected to the store, it renders again with a new value of auth.user.kind. If the value is "p", the route corresponding to the second screen gets removed from the navigation state and the tab gets hidden.

This is it! We end up with a tab navigation that can display tabs dynamically depending on values in our Redux store.

Upvotes: 2

Related Questions