Behind the Scenes: Understanding the Inner Workings of Custom Tab Bars with React Navigation

Behind the Scenes: Understanding the Inner Workings of Custom Tab Bars with React Navigation

Introduction

The default tab bar in React Navigation is rather basic, and attempting to customize it can lead to a messy outcome. Fortunately, there is a straightforward way to implement custom tabs using the Material Top Tab Bar in React Native with React Navigation. The setup process is quick and can be completed in just a few minutes. Today in this blog we are going to understand how it works.

I am assuming you did the setup of React Navigation in your project, if not follow the steps to set up React Navigation and create a native stack navigator, it's pretty easy.

To have a tab bar on the top of the screen, that lets us switch between routes by tapping the tabs or swiping horizontally, we have Material Tob Tabs Navigator by React Navigation. Please refer docs.

This is how my App.tsx looks. It is always a good idea to create separate files for your stack screens. Once your project grows and there are multiple screens the App.tsx becomes cluttered!

import {NavigationContainer} from '@react-navigation/native';
// ...
function App(): React.JSX.Element {
  return (
    <NavigationContainer>
      <SafeAreaView style={styles.mainContainer}>
        <Navigation />
      </SafeAreaView>
    </NavigationContainer>
  );
}

Navigation.tsx

import {createNativeStackNavigator} from '@react-navigation/native-stack';
// ...
const Navigation = () => {
  const Stack = createNativeStackNavigator();
  return (
    <Stack.Navigator initialRouteName="Home">
      <Stack.Screen
        options={{headerShown: false}}
        name="Home"
        component={Homescreen}
      />
    </Stack.Navigator>
  );
};
export default Navigation;

Homescreen.tsx

import {createMaterialTopTabNavigator} from '@react-navigation/material-top-tabs';
//...
const Homescreen = () => {
  const Tab = createMaterialTopTabNavigator();
  return (
    <Tab.Navigator
      tabBar={props => <CustomTabBar {...props} />}
      style={styles.tabStyle}>
      <Tab.Screen name="Demo1" component={DemoScreen1} />
      <Tab.Screen name="Demo2" component={DemoScreen2} />
      <Tab.Screen
        options={{title: 'Demo Screen 3'}}
        name="Demo3"
        component={DemoScreen3}
      />
    </Tab.Navigator>
  );
};

Here is the custom tabBar function

CustomTabBar.tsx

import {Animated, View, TouchableOpacity} from 'react-native';

export default function CustomTabBar({state, descriptors, navigation, position}: any) {
  return (
    <View style={{flexDirection: 'row'}}>
      {state.routes.map((route, index) => {
        const {options} = descriptors[route.key];
        const label =
          options.tabBarLabel !== undefined
            ? options.tabBarLabel
            : options.title !== undefined
            ? options.title
            : route.name;

        const isFocused = state.index === index;

        const onPress = () => {
          const event = navigation.emit({
            type: 'tabPress',
            target: route.key,
            canPreventDefault: true,
          });

          if (!isFocused && !event.defaultPrevented) {
            navigation.navigate(route.name, route.params);
          }
        };

        const onLongPress = () => {
          navigation.emit({
            type: 'tabLongPress',
            target: route.key,
          });
        };

        const inputRange = state.routes.map((_, i) => i);
        const opacity = position.interpolate({
          inputRange,
          outputRange: inputRange.map(i => (i === index ? 1 : 0)),
        });

        return (
          <TouchableOpacity
            key={route.key}
            accessibilityRole="button"
            accessibilityState={isFocused ? {selected: true} : {}}
            accessibilityLabel={options.tabBarAccessibilityLabel}
            testID={options.tabBarTestID}
            onPress={onPress}
            onLongPress={onLongPress}
            style={{flex: 1}}>
            <Animated.Text style={{color: 'black'}}>{label}</Animated.Text>
          </TouchableOpacity>
        );
      })}
    </View>
  );
}

Let us break down the function.

Internal working

These 4 parameters are important while building a Custom tab component:

State

  • Imagine this as a snapshot of where you are in your app. It keeps track of which screen you're currently viewing and provides a list of all possible routes.

  • If you just console.log the state, you will get this object, each time you change a screen the state will get updated.

  • history -> The history is like a record or list of all the screens you have visited in the past. Not all parts of the app have this history feature—only certain parts, like tabs or drawers, might keep track of where you've been

  • Index -> Position of the focused screen in the routes array.

  • routesNames -> Name of the screen defined in the navigator.

// console.log(state)  
{
    "history": [
      {
        "key": "Demo1-tDHcPmmRmdJs837m1-olK",
        "type": "route"
      },
      {
        "key": "Demo3--0m-QGhSnWqixDeKdoS7D",
        "type": "route"
      }
    ],
    "index": 2,
    "key": "tab-rsFLKC9b4gow_h2x6QelX",
    "routeNames": [
      "Demo1",
      "Demo2",
      "Demo3"
    ],
    "routes": [
      {
        "key": "Demo1-tDHcPmmRmdJs837m1-olK",
        "name": "Demo1",
        "params": undefined
      },
      {
        "key": "Demo2-EimNrVsIA-ZXW8mCSSxua",
        "name": "Demo2",
        "params": undefined
      },
      {
        "key": "Demo3--0m-QGhSnWqixDeKdoS7D",
        "name": "Demo3",
        "params": undefined
      }
    ],
    "stale": false,
    "type": "tab"
  }

Descriptors

  • Think of descriptors as a set of instructions for each route. It tells the app how to handle and display each screen, including any special instructions for the tab bar.

      // console.log(descriptors)
      "Demo3-4vfsJoiMZndxF-MRA2cpj": {
        "navigation": {
          "addListener": [
            FunctionaddListener
          ],
          "isFocused": [
            FunctionisFocused
          ],
          "navigate": [
            Functionanonymous
          ],
          "replace": [
            Functionanonymous
          ],
      ...and many more
        },
        "options": {
          "title": "Demo SCreen 3"
        },
        "render": [
          Functionrender
        ],
        "route": {
          "key": "Demo3-4vfsJoiMZndxF-MRA2cpj",
          "name": "Demo3",
          "params": undefined
        }
      }
    

Navigation

  • The navigation prop is like a remote control for moving around in your app. It gives you buttons to go to different screens, go back, and perform other actions related to navigation.

  •       // console.log(navigation)
         {
            "addListener": [
              FunctionaddListener
            ],
            "dispatch": [
              Functiondispatch
            ],
            "pop": [
              Functionanonymous
            ],
            "popToTop": [
              Functionanonymous
            ],
            "push": [
              Functionanonymous
            ],
            "replace": [
              Functionanonymous
            ],
          //....and many more
          }
    

Position

  • The position helps in creating cool effects, like making tabs fade in or out based on where you are on the list of screens.

  •   // console.log(position)
      0.8722222447395325
    

Now that we know why each parameter is important, it is easy to understand the code. Let us have a look at onPress function.

const onPress = () => {
    const event = navigation.emit({
        type: 'tabPress',
        target: route.key,
        canPreventDefault: true,
        });
        if (!isFocused && !event.defaultPrevented) {
            navigation.navigate(route.name, route.params);
          }
    };

The default behavior is what normally happens when you tap a tab: it takes you to the screen connected to that tab. The code checks if anything wants to stop this default action before allowing it to happen.

Conclusion

In this post, we've explored how a custom tab bar function works in React Navigation, focusing on the Material Top Tab Bar in React Native. By breaking down the essential parameters—state, descriptors, navigation, and position—we've unveiled the function's operation.

If you have any questions or just want to talk about tech in general, feel free to reach out to me. Hopefully, this helps you understand React Navigation better.