Routing in mobile applications presents more then mere navigation between routes (screens). It consists of functionalities and styles which are divided across platforms. Generally, routing systems inside native applications has its own look and feel as well as well defined UX which differs from android and iOS platforms.
In order to achieve this “native feel”, RN community implemented a React Navigation library. It’s main focus presents “native-look-and-feel” while keeping the performance and extensibility.
Official documentation
Official documentation has a lot of information and contains more than a few code examples of all important parts. Make sure to get familiar with it before continuing to read further.
1. Navigation file structure
In order to keep your navigation maintainable and easy to upgrade, here are some guidelines which rely on library main concepts: Stack, Tab, Drawer and Parameters.
- navigation/
- stacks/
- tabs/
- drawer/
- params/
- Navigator.tsx
Navigator.tsx
1.1 Navigator.tsx
is a React functional component which defines navigation entry point. It should contain navigation structure wrapped inside NavigationContainer
:
const Navigator: React.FC<INavigatorProps> = (props) => {
return <NavigationContainer>// NAVIGATION STRUCTURE</NavigationContainer>;
};
1.2 Stacks
All navigation stacks should be wrapped as functional components inside navigation/stacks
.
For example, most common application structure consits of authentication (login, register) and main (home, etc..) workflow.
In React Navigation terms this means that you should split your navigation stacks between those 2 flows: AuthenticationStack
and HomeStack
. Even though we could use only one stack with all screens, this is a bad practice in mobile development and makes the app harder to maintain as your application grows.
// AuthenticationStack.tsx
const AuthenticationStack = createStackNavigator(AuthenticationStackParamList);
const AuthenticationStack: React.FC<IAuthenticationStackProps> = (props) => {
return (
<Stack.Navigator>
<Stack.Screen component={Login} name="Login" />
<Stack.Screen component={Register} name="Register" />
// ... other screens in "Authentication" flow
</Stack.Navigator>
);
};
// HomeStack.tsx
const HomeStack = createStackNavigator(HomeStackParamList);
const HomeStack: React.FC<IHomeStackProps> = (props) => {
return (
<Stack.Navigator>
<Stack.Screen component={Home} name="Home" />
// ... other screens in "Home" flow
</Stack.Navigator>
);
};
IMPORTANT: NavigationContainer
component accepts single child component. This is not an issue if your application uses Tab
or Drawer
navigation. If you use only Stacks
you need to create RootStack
component which will then target all other Stacks
inside separate Stack.Screen
components.
Using example above this Navigator.tsx
would look like:
const RootStack = createStackNavigator();
const Navigator: React.FC<INavigatorProps> = (props) => {
return (
<NavigationContainer>
<RootStack.Navigator>
<RootStack.Screen
component={AuthenticationStack}
name="AuthenticationStack"
/>
<RootStack.Screen component={HomeStack} name="HomeStack" />
</RootStack.Navigator>
</NavigationContainer>
);
};
1.3 Tabs
Usually, applications have one navigation tab which should be in navigation/tabs
. When using tab navigation, each Tab.Screen
component should target a stack with related screens.
const Tab = createTabNavigator();
const TabNavigator: React.FC<ITabNavigatorProps> = (props) => {
return (
<Tab.Navigator>
<Tab.Screen component={AuthenticationStack} name="AuthenticationStack" />
<Tab.Screen component={HomeStack} name="HomeStack" />
</Tab.Navigator>
);
};
1.4 Drawer
Applications with drawer navigation should be defined in navigation/drawer
. When using drawer navigation, each Drawer.Screen
component should target a stack or single related screen.
const Drawer = createDrawerNavigator();
const DrawerNavigator: React.FC<IDrawerNavigatorProps> = (props) => {
return (
<Drawer.Navigator>
<Drawer.Screen
component={AuthenticationStack}
name="AuthenticationStack"
/>
<Drawer.Screen component={HomeStack} name="HomeStack" />
</Drawer.Navigator>
);
};
1.4 Parameters (Typescript)
To fully use the power of Typescript, navigation/params
folder should contain types and interfaces for all stacks used inside the application. Since React Navigation handles Tab
and Drawer
navigations in the background, there is no need for any type definitions.
Each stack should have its parameters defined in a separate file. The parameters file consists of 3 main parts:
- List of screens used inside of stack with related
route
params defined astype
. - Screen
route
parameters defined asinterface
- Stack screen props defined as
type
Example:
1.
export type HomeStackParamList = {
Home: IHomeRouteProps;
// ... other HomeStack screens
};
2.
export interface IHomeRouteProps {
propName: propValue;
...
};
3.
export type HomeStackScreenProps<T extends keyof HomeStackParamList> = {
navigation: StackNavigationProp<HomeStackParamList, T>;
route: RouteProp<HomeStackParamList, T>;
};
Each screen inside the application inherits navigation
and route
props from React Navigation. Type (3) should be used as a typing for screen props which basically gives code completion for all navigation and routing parameters inside of the screen.
Correct use:
- Create stack with related types:
const HomeStack = createStackNavigator<HomeStackParamsList>();
- Add screen types to each screen components props:
const Home: React.FC<HomeStackScreenProps<'Home'>> = ({ navigation, route}) => (...);
IMPORTANT: Above route types (2) won't work for nested stacks. Correct route types:
type NestedRouteParams<T> = {
[K in keyof T]: undefined extends T[K]
? { screen: K; params?: T[K] }
: { screen: K; params: T[K] };
}[keyof T];
Example:
HomeStack
contains Home
screen and UserStack
, which then contains Profile
screen. In that case our params list type looks like:
export type HomeStackParamList = {
Home: IHomeRouteProps;
ProfileStack: NestedRouteParams<ProfileStackParamList>;
};
This way we can easily navigate to each screen inside ProfileStack
from anywhere inside HomeStack
:
navigation.navigate("ProfileStack", {
screen: "Profile",
params: { /* params */ }, // route params
});
Tips
-
Header
component height differs between android & iOS.android: 56, iOS: 44 + insetTop
.insetTop
equals to size of the status bar + top notch. To get the exact size of the notch (top, bottom) you should usereact-native-safe-area-context
library. Library implementsuseSafeAreaInsets
hook which returnsinset
object containing definitions of top and bottom insets:
const { top, bottom } = useSafeAreInsets()
Since newer android devices started to add notch as well, you should always define your Header
height using height + topInset
value.