8 min read | Feb 22, 2024

Nested Navigation: Combining multiple types of navigation in React Native

This article shows you how to mix tab and stack navigation in React Native to make moving around your app easy. We'll walk you through the steps to set up and blend these two types of navigation, making your app's navigation feel natural and straightforward.

Why Nested Navigation Can Be Useful?

Imagine this situation: your application currently has 2 screens within the Tab navigation:

  • Images
  • Contacts

Let's say you want to add a few more screens to view more details or for certain actions:

  • Image Details
  • Add new contant

However, you also want to do that without adding them to the Tab Navigation because users should navigate to them after selecting an image or after pressing an action button.

In this situation, one of the best solutions would be to combine two types of navigation - Tab and Stack Navigation.

By doing this, you will have more solutions for your navigation problems, and you will separate main screens from, let's say, action screens like forms, details, etc.

Step-by-Step Implementation

Setup a Blank Project

First, we need to create an empty project. I will use the TypeScript template:

npx create-expo-app -t expo-template-blank-typescript combining-navigation-example

Next, go to the project's directory:

cd combining-navigation-example

And add the necessary libraries:

npm install @react-navigation/native @react-navigation/native-stack @react-navigation/stack @react-navigation/bottom-tabs
npm install react-native-safe-area-context react-native-gesture-handler

After that, you can use this command to run the app on the iOS simulator:

npm run ios

The terminal may ask you this question:

What would you like your iOS bundle identifier to be?

Press Enter and wait until the build is done. At this moment, you've set up everything, and you can start experimenting with the app. Let's build some navigation!

Creating Project Structure

After adding all the libraries, it's time to structure our project a bit. It won't be too large in its current version, but it's always worth keeping the project organized. To start, I recommend adding a src folder, and it is in this folder that the entire project structure should be placed. In our case, to maintain order, I would add 2 subfolders in it:

  • navigation
    - this will contain our main file in which the navigation is located
  • screens
    - here we will place views for a given screen in our application

here is the graph of the project structure:

src/
├── navigation/
│   ├── index.ts
│   ├── Navigation.component.tsx
│   └── Navigation.types.ts
└── screens/
    ├── AddNewContactScreen.component.tsx
    ├── ContactsScreen.component.tsx
    ├── ImageDetailsScreen.component.tsx
    ├── ImagesScreen.component.tsx
    └── index.ts

At this moment, we have two screens for the Tab Navigation and another two for Stack Navigation. Screens in Tab Navigation will serve as our main screens, where we will display general data, but image details and the add new contact form will be on separate screens.

Setting Up Screens

I don't want to get too complicated with the code, so I'll create basic screens that include a title and a navigation button. Be aware, the screens in the Stack Navigation differ from those in the Tab Navigation.

At first, let's define types for the navigation:

// /navigation/Navigation.types.ts
import { BottomTabScreenProps } from '@react-navigation/bottom-tabs';
import { CompositeScreenProps, NavigatorScreenParams } from '@react-navigation/native';
import { NativeStackScreenProps } from '@react-navigation/native-stack';
import { StackScreenProps } from '@react-navigation/stack';

// Declare type for bottom tab navigation
export type TabNavigatorScreenProps = {
  Contacts: undefined;
  Images: undefined;
};

// Declare type for stack navigation
export type StackNavigatorScreenProps = {
  TabNavigator: NavigatorScreenParams<TabNavigatorScreenProps>;
  AddNewContact: undefined;
  ImageDetails: { photoId: string };
};

// Declare type for tab navigation screens
export type TabScreenNavigationProps<T extends keyof TabNavigatorScreenProps> = CompositeScreenProps<
  BottomTabScreenProps<TabNavigatorScreenProps, T>,
  NativeStackScreenProps<StackNavigatorScreenProps>
>;

// Declare type for stack navigation screens
export type StackScreenNavigationProps<T extends keyof StackNavigatorScreenProps> = StackScreenProps<
  StackNavigatorScreenProps,
  T
>;

And now we can create our screens for Stack Navigation:

// /screens/AddNewContactScreen.component.tsx
import { Pressable, StyleSheet, Text, View } from 'react-native';

import { StackScreenNavigationProps } from '../navigation/Navigation.types';

export const AddNewContactScreen: React.FC<StackScreenNavigationProps<'AddNewContact'>> = ({ navigation }) => {
  const navigateToImagesScreen = () => navigation.navigate('TabNavigator', { screen: 'Images' });

  return (
    <View style={styles.container}>
      <Text>Add New Contact Screen</Text>
      <Pressable style={styles.button} onPress={navigation.goBack}>
        <Text>Go Back</Text>
      </Pressable>
      <Pressable style={styles.button} onPress={navigateToImagesScreen}>
        <Text>Go To Images Screen</Text>
      </Pressable>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center',
  },
  button: {
    backgroundColor: '#ade',
    color: 'white',
    textAlign: 'center',
    paddingHorizontal: 20,
    paddingVertical: 10,
    borderRadius: 5,
    fontSize: 24,
    margin: 5,
  },
});
// /screens/ImageDetailsScreen.component.tsx
import { Pressable, StyleSheet, Text, View } from 'react-native';

import { StackScreenNavigationProps } from '../navigation/Navigation.types';

export const ImageDetailsScreen: React.FC<StackScreenNavigationProps<'ImageDetails'>> = ({ navigation, route }) => {
  const { photoId } = route.params;

  const navigateToContactsScreen = () => navigation.navigate('TabNavigator', { screen: 'Contacts' });

  return (
    <View style={styles.container}>
      <Text>Add New Contact Screen</Text>
      <Text>Photo ID: {photoId}</Text>
      <Pressable style={styles.button} onPress={navigation.goBack}>
        <Text>Go Back</Text>
      </Pressable>
      <Pressable style={styles.button} onPress={navigateToContactsScreen}>
        <Text>Go To Contacts Screen</Text>
      </Pressable>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center',
  },
  button: {
    backgroundColor: '#ade',
    color: 'white',
    textAlign: 'center',
    paddingHorizontal: 20,
    paddingVertical: 10,
    borderRadius: 5,
    fontSize: 24,
    margin: 5,
  },
});

So far, so good. Let's now create screens for our Tab Navigation:

// /screens/ContactsScreen.component.tsx
import { Pressable, StyleSheet, Text, View } from 'react-native';

import { TabScreenNavigationProps } from '../navigation/Navigation.types';

export const ContactsScreen: React.FC<TabScreenNavigationProps<'Contacts'>> = ({ navigation }) => {
  const navigateToAddNewContactScreen = () => navigation.navigate('AddNewContact');

  return (
    <View style={styles.container}>
      <Text>Contacts Screen</Text>
      <Pressable style={styles.button} onPress={navigateToAddNewContactScreen}>
        <Text>Add New Contact</Text>
      </Pressable>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center',
  },
  button: {
    backgroundColor: '#ade',
    color: 'white',
    textAlign: 'center',
    paddingHorizontal: 20,
    paddingVertical: 10,
    borderRadius: 5,
    fontSize: 24,
    margin: 5,
  },
});
// /screens/ImagesScreen.component.tsx
import { Pressable, StyleSheet, Text, View } from 'react-native';

import { TabScreenNavigationProps } from '../navigation/Navigation.types';

export const ImagesScreen: React.FC<TabScreenNavigationProps<'Images'>> = ({ navigation }) => {
  const navigateToAddNewContactScreen = (photoId: string) => navigation.navigate('ImageDetails', { photoId });

  return (
    <View style={styles.container}>
      <Text>Images Screen</Text>
      <Pressable style={styles.button} onPress={() => navigateToAddNewContactScreen('your_photo_id')}>
        <Text>Image Details</Text>
      </Pressable>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center',
  },
  button: {
    backgroundColor: '#ade',
    color: 'white',
    textAlign: 'center',
    paddingHorizontal: 20,
    paddingVertical: 10,
    borderRadius: 5,
    fontSize: 24,
    margin: 5,
  },
});

And now we can export all of our screens from index.ts, this is only for keep everything clean:

// /navigation/index.ts
export { AddNewContactScreen } from './AddNewContactScreen.component';
export { ContactsScreen } from './ContactsScreen.component';
export { ImageDetailsScreen } from './ImageDetailsScreen.component';
export { ImagesScreen } from './ImagesScreen.component';

Great! We have now set up our project, established its structure, defined our navigation types, and implemented the code for the screens. The last step lies ahead of us.

Building navigation

The final step is to bring all these screens together and set up the navigation. Firstly, we need to create the bottom Tab Navigation:

import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';

import { ContactsScreen, ImagesScreen } from '../screens';
import { TabNavigatorScreenProps } from './Navigation.types';

const Tab = createBottomTabNavigator<TabNavigatorScreenProps>();

export const MainScreens: React.FC = () => {
  return (
    <Tab.Navigator>
      <Tab.Screen name="Contacts" component={ContactsScreen} />
      <Tab.Screen name="Images" component={ImagesScreen} />
    </Tab.Navigator>
  );
};

Next, we can create Stack Navigation and include the Tab Navigator within it:

// /navigation/Navigation.component.tsx
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';

import { AddNewContactScreen, ContactsScreen, ImageDetailsScreen, ImagesScreen } from '../screens';
import { StackNavigatorScreenProps, TabNavigatorScreenProps } from './Navigation.types';

const Stack = createStackNavigator<StackNavigatorScreenProps>();

export const MainNavigator: React.FC = () => {
  return (
    <NavigationContainer>
      <Stack.Navigator>
        <Stack.Screen name="TabNavigator" component={MainScreens} options={{ headerShown: false }} />
        <Stack.Group>
          <Stack.Screen name="ImageDetails" component={ImageDetailsScreen} />
          <Stack.Screen name="AddNewContact" component={AddNewContactScreen} />
        </Stack.Group>
      </Stack.Navigator>
    </NavigationContainer>
  );
};

const Tab = createBottomTabNavigator<TabNavigatorScreenProps>();

const MainScreens: React.FC = () => {
  return (
    <Tab.Navigator>
      <Tab.Screen name="Contacts" component={ContactsScreen} />
      <Tab.Screen name="Images" component={ImagesScreen} />
    </Tab.Navigator>
  );
};

The last step is to export our MainNavigator from index.ts:

// /navigation/index.ts
export { MainNavigator } from './Navigation.component';

And import it in App.tsx

// App.tsx
import { MainNavigator } from './src/navigation';

export default function App() {
  return <MainNavigator />;
}

Following these steps, you should see the main screen with bottom tabs, and on each screen, additional buttons will navigate you to the Stack screen.

Conclusion

In this guide, we showed how to use both tab and stack navigation in React Native apps. We went through starting a project, setting up navigation, and making screens. This helps make app navigation easier to use and more organized.

Good navigation makes apps better for users. React Native gives developers the tools they need to make complex navigation simple.

Keep trying new things and adjusting your app. This is key to making apps that people like to use. Keep learning and have fun making your apps better!

Dependencies versions - package.json

{
  "dependencies": {
    "@react-navigation/bottom-tabs": "^6.5.12",
    "@react-navigation/native": "^6.1.10",
    "@react-navigation/native-stack": "^6.9.18",
    "@react-navigation/stack": "^6.3.21",
    "expo": "~50.0.7",
    "expo-status-bar": "~1.11.1",
    "react": "18.2.0",
    "react-native": "0.73.4",
    "react-native-gesture-handler": "^2.15.0",
    "react-native-safe-area-context": "^4.9.0"
  }
}