React Navigation with Typescript for nested navigators

The Context

Working on a refactor in our React Native codebase using React Navigation for routing, I found that some of our types were inconsistent when using nested navigators.
đź’ˇ
Nesting navigators is not designed to be a way to organise our code - it’s a way to organise our navigation; it is not supported to navigate from one navigator (e.g. StackNavigator or BottomTabNavigator) to another. Having many nested navigators makes it difficult to react to changing requirements - only split into different navigators if the two are completely siloed sections of your app.

The Problem

Our app has a BottomTabNavigator with some of the tabs being their own stack within the app (no expected navigation between the them):
// BottomTabNavigator.tsx import { createBottomTabNavigator } from "@react-navigation/bottom-tabs"; import { Tab1Screen } from "../Tab1"; import { Tab2Stack } from "../Tab2"; enum BottomTabNavigatorRoutes { Tab1 = "Tab1", Tab2 = "Tab2", } type BottomTabNavigatorParamList = { [BottomTabNavigatorRoutes.Tab1]: undefined; [BottomTabNavigatorRoutes.Tab2]: undefined; }; const Tabs = createBottomTabNavigator(); export const BottomTabNavigator = () => { return ( <Tabs.Navigator initialRouteName={BottomTabNavigatorRoutes.Tab1} > <Tabs.Screen name={BottomTabNavigatorRoutes.Tab1} component={Tab1Screen} /> <Tabs.Screen name={BottomTabNavigatorRoutes.Tab2} component={Tab2Stack} /> </Tabs.Navigator> ); };
Let’s say that Tab1 is a regular screen and Tab2 is its own stack:
// Tab1.tsx import { FC } from "react"; import { Screen } from "components/Screen"; export const Tab1Screen: FC = () => ( <Screen> Tab 1 </Screen> ); // Tab2.tsx import { FC } from "react"; import { NestedScreen1 } from "../NestedScreen1"; import { NestedScreen2 } from "../NestedScreen2"; import { createStackNavigator } from "@react-navigation/stack"; enum Tab2StackRoutes { NestedScreen1 = "NestedScreen1", NestedScreen2 = "NestedScreen2", } type Tab2StackbNavigatorParamList = { [Tab2StackRoutes.NestedScreen1]: undefined; [Tab2StackRoutes.NestedScreen2]: undefined; }; export const Tab2 = createStackNavigator(); export const Tab2Stack: FC = () => ( <Tab2.Navigator initialRouteName={Tab2StackRoutes.NestedScreen1}> <Tab2.Screen name={Tab2StackRoutes.NestedScreen1} component={NestedScreen} /> <Tab2.Screen name={Tab2StackRoutes.NestedScreen2} component={NestedScreen} /> </Tab2.Navigator> );
This will work, but if we want to access the navigation or route params that React Navigation passed down to navigate somewhere or access some parameters that are passed through with navigation then we will lose our type safety. Additionally, if we have some partial typing, or ES-lint rules which prevent assignment of any, the inconsistent types will block us from merging code in.

The Solution

The solution is to follow the docs for nested navigation - CompositeScreenProps!

Stack navigation params

To add typing to the navigators, pass the param list in as a generic:
- const Tabs = createBottomTabNavigator(); + const Tabs = createBottomTabNavigator<BottomTabNavigatorParamList>(); - export const Tab2 = createStackNavigator(); + export const Tab2 = createStackNavigator<Tab2StackbNavigatorParamList>();
This tells Typescript what parameters should be passed when calling navigation.navigate on a particular screen.

Screen props

Use the correct ScreenProps type from React Navigation to get the correctly typed navigation and route props (this could also be StackScreenProps instead - it depends on the outer navigator):
+ import { BottomTabScreenProps } from "@react-navigation/bottom-tabs"; + type Tab1Props = BottomTabScreenProps< + BottomTabNavigatorParamList, + BottomTabNavigatorRoutes.Tab1Props + >; - export const Tab1Screen: FC = () => ( + export const Tab1Screen: FC<Tab1Props> = ({ navigation, route }) => (
This tells our screen which navigator it sits within and therefore what params it gets passed.

Nested navigators

From the docs:
When you nest navigators, the navigation prop of the screen is a combination of multiple navigation props.
Tab2Stack needs to know that it’s a tab within the the BottomTabNavigator (so we can do navigation.jumpTo other screens in the navigator), but also that it’s itself a StackNavigator with a navigation prop that can navigate between the screens in its stack.
Tab2Stack is a StackNavigator, so it actually has all the props of a navigator, but because it’s nested, we also want it to know about the props it’s getting passed when we navigate to it. We use the React Navigation CompositeScreenProps type to do this:
+ import type { CompositeScreenProps } from '@react-navigation/native'; + import { BottomTabScreenProps } from "@react-navigation/bottom-tabs"; + type Tab2StackProps = CompositeScreenProps< + BottomTabScreenProps<BottomTabNavigatorParamList, BottomTabNavigatorRoutes.Tab2> + StackScreenProps<Tab2StackbNavigatorParamList>, + >; - export const Tab2: FC = () => ( + export const Tab2: FC<Tab2StackProps> = ({ navigation, routes }) => (
According to the docs, the first parameter of CompositeScreenProps is the primary navigator and the second is the secondary (parent) navigator. Ensure that the primary navigator includes the screen name (second parameter in BottomTabScreenProps) or some type safety is lost (you might be able to render this component to a screen which shouldn’t accept it.
The BottomTabNavigator rendering this component, now has matching props with the component it renders and type checking is in place to ensure this component is for the correct screen.
Because we have nested two different types of navigators, the navigation prop has access to the methods for the tab navigator and the stack navigator.

Multiple nested navigators

Here we need to nest the composite screen props multiple times. An example from the docs:
type ProfileScreenProps = CompositeScreenProps< BottomTabScreenProps<TabParamList, 'Profile'>, CompositeScreenProps< StackScreenProps<StackParamList>, DrawerScreenProps<DrawerParamList> > >;
At this point it’s probably a good idea to use some named types:
// Lives in an outer file where the outer screen is type OuterScreenProps = CompositeScreenProps< StackScreenProps<StackParamList>, DrawerScreenProps<DrawerParamList> > type NestedScreenProps = CompositeScreenProps< BottomTabScreenProps<TabParamList, 'TabRouteName'>, OuterScreenProps >;

The Code

The final code looks like this:
// BottomTabNavigator.tsx import { createBottomTabNavigator } from "@react-navigation/bottom-tabs"; import { Tab1Screen } from "../Tab1"; import { Tab2Stack } from "../Tab2"; enum BottomTabNavigatorRoutes { Tab1 = "Tab1", Tab2 = "Tab2", } type BottomTabNavigatorParamList = { [BottomTabNavigatorRoutes.Tab1]: { itemId: number }; [BottomTabNavigatorRoutes.Tab2]: undefined; }; const Tabs = createBottomTabNavigator<BottomTabNavigatorParamList>(); export const BottomTabNavigator = () => { return ( <Tabs.Navigator initialRouteName={BottomTabNavigatorRoutes.Tab1} > <Tabs.Screen name={BottomTabNavigatorRoutes.Tab1} component={Tab1Screen} initialParams={{ itemId: 1 }} // We can add a default param /> <Tabs.Screen name={BottomTabNavigatorRoutes.Tab2} component={Tab2Stack} /> </Tabs.Navigator> ); };
// Tab1.tsx import { FC } from "react"; import { Screen } from "components/Screen"; import { BottomTabScreenProps } from "@react-navigation/bottom-tabs"; import { BottomTabNavigatorParamList, BottomTabNavigatorRoutes, } from "../BottomTabNavigator"; type Tab1Props = BottomTabScreenProps< BottomTabNavigatorParamList, BottomTabNavigatorRoutes.Tab1Props >; export const Tab1Screen: FC<Tab1Props> = ({ navigation, route }) => ( <Screen> {route.params.itemId} // correctly typed params </Screen> );
// Tab2.tsx import { FC } from "react"; import { NestedScreen1 } from "../NestedScreen1"; import { NestedScreen2 } from "../NestedScreen2"; import { createStackNavigator } from "@react-navigation/stack"; import type { CompositeScreenProps } from '@react-navigation/native'; import { BottomTabScreenProps } from "@react-navigation/bottom-tabs"; import { BottomTabNavigatorParamList, BottomTabNavigatorRoutes, } from "../BottomTabNavigator"; enum Tab2StackRoutes { NestedScreen1 = "NestedScreen1", NestedScreen2 = "NestedScreen2", } type Tab2StackbNavigatorParamList = { [Tab2StackRoutes.NestedScreen1]: undefined; [Tab2StackRoutes.NestedScreen2]: undefined; }; type Tab2StackProps = CompositeScreenProps< BottomTabScreenProps<BottomTabNavigatorParamList, BottomTabNavigatorRoutes.Tab2> StackScreenProps<Tab2StackbNavigatorParamList>, >; export const Tab2 = createStackNavigator<Tab2StackbNavigatorParamList>(); export const Tab2Stack: FC<Tab2StackProps> = ({ navigation, route }) => ( <Tab2.Navigator initialRouteName={Tab2StackRoutes.NestedScreen1}> <Tab2.Screen name={Tab2StackRoutes.NestedScreen} component={NestedScreen1} /> <Tab2.Screen name={Tab2StackRoutes.NestedScreen} component={NestedScreen2} /> </Tab2.Navigator> );
Let me know if this helped, or if I got anything wrong (typescript seems to be working and giving me useful errors with this setup) đź‘Ť