Introduction

Creating an intuitive navigation UI is essential for any mobile app. Because we build custom software at SmartLogic, most of our mobile apps require different navigation setups, but they often have a lot in common. We use React Navigation which makes it easy for users to move across different screens. Built-in options allow us to quickly add UI features while also being fully customizable. In this post, I’ll walk through a basic React Navigation pattern we’ve used and the steps to get it up and running.

Below is an example of the architecture of common patterns we use. We have the main navigation for our app, different navigation stacks (more on this later), screens and a tab navigation setup as well.

File tree

OurApp \ App.js Containers \ RootContainer.js HomeScreen.js LoginScreen.js ImportContactsScreen.js ChatListScreen.js ProfileScreen.js TransactionListScreen.js Navigation \ AppNavigator.js TabNavigator.js HomeStack.js ChatStack.js TranactionsStack.js components \ BackArrow.js

Installation

Installing react navigation into your project is easy. Here are the main commands to get started, straight from the docs.

In your root project directory run:

yarn add react-navigation

If you are using Expo managed workflow you do not need to add the following as it’s already included and can skip down to the next section.

Then:

yarn add react-native-gesture-handler

Link RN Gesture Handler by running:

react-native link react-native-gesture-handler

Additional Android Gesture Handler setup:

your MainActivity.java should look like the following; you'll need to add the lines that have a +:

package com.reactnavigation.example; import com.facebook.react.ReactActivity; + import com.facebook.react.ReactActivityDelegate; + import com.facebook.react.ReactRootView; + import com.swmansion.gesturehandler.react.RNGestureHandlerEnabledRootView; public class MainActivity extends ReactActivity { @Override protected String getMainComponentName() { return "Example"; } + @Override + protected ReactActivityDelegate createReactActivityDelegate() { + return new ReactActivityDelegate(this, getMainComponentName()) { + @Override + protected ReactRootView createRootView() { + return new RNGestureHandlerEnabledRootView(MainActivity.this); + } + }; + } }

Navigation Architecture

Root Component

App.js is the entry point for the app, it is the top level component. In it we will include a <RootContainer /> component which will hold the app navigation.

file: OurApp/App.js

import React, { Component } from 'react' import RootContainer from './app/containers/RootContainer' export default class App extends Component { render () { return ( <RootContainer /> ) } }

<Root Container />

Our RootContainer will holds the app’s root navigation

file path: OurApp/app/containers/RootContainer.js

import React, { PureComponent } from 'react' import { StatusBar, View } from 'react-native' import AppNavigator from '../navigation/AppNavigator' export default class RootContainer extends PureComponent { render () { return ( <View style={{ flex: 1 }}> <StatusBar /> <AppNavigator /> </View> ) } }

App Navigator

App Navigator is the base of our app navigation. The initial route is set by initialRouteName and is where the app starts when users first open it. Here we are starting on the HomeStack , which is a nested navigation stack.

Nested navigation stacks, or just stacks, are a way of organizing our screens into groups. We group together screens that follow a common flow as users navigate the app.

We’ve defined the constant AppNavigator for easy export which uses createStackNavigator() to create routes for each of the stacks we'll use and createAppContainer() to create the navigation container.

App Navigator is our stack navigator. We’re essentially creating a stack of all the stacks and then turning it into our main navigation container.

file path: OurApp/app/navigation/AppNavigator.js

import { createStackNavigator, createAppContainer } from 'react-navigation' import HomeStack from './HomeStack' import TabNavigator from './TabNavigator' import ChatStack from './ChatStack' import TransactionStack from './TransactionStack' const AppNavigator = createStackNavigator({ HOME_STACK: { screen: HomeStack }, TAB_NAVIGATION: { screen: TabNavigator } }, { initialRouteName: 'HOME_STACK', headerMode: 'none' }) export default createAppContainer(AppNavigator)

createStackNavigator returns a React component, which we are exporting and use in the <RootContainer /> . For organization, I’ve put my Stack Navigator in a separate file.

In it, I’ve defined a route for each of the different stacks we have and set the screen of each route to point to the stack component which defines the screens for each stack. We usually put stack names in screaming snake case just for distinction, this is not required.

Individual stack components

In each of the stack components, we are linking screens that would logically go together to form a navigation flow.

We use createStackNavigator again to create a route for each screen in the stack. And because we’re keeping our route name the same as our screen names, we don’t have to set the screen separately.

file: OurApp/app/navigation/HomeStack.js folder:

import { createStackNavigator } from 'react-navigation' import HomeScreen from '../containers/HomeScreen' import LoginScreen from '../containers/LoginScreen' import ImportContactsScreen from '../containers/ImportContactsScreen' const HomeStack = createStackNavigator({ HomeScreen, LoginScreen, ImportContactsScreen }, { initialRouteName: 'HomeScreen', headerMode: 'none' }) export default HomeStack

Tab navigator

We have relatively few stacks and screens to move through in this app, so we use bottom tab navigation via createBottomTabNavigator() once users are signed in to move through the three main stacks.

We’re using icons for users to press to get to each of the three stacks in the tab navigator. The navigator will remain visible at the bottom on each screen the user navigates to.

We define the icons at the top, rendering React Native Vector Icons components, which we’ve already included in our app and in tabBarOptions we set the active/inactive icon colors which come from the app/themes/Colors file where all of our app colors are defined.

file: OurApp/app/navigation/TabNavigator.js

import React from 'react' import { createBottomTabNavigator, createAppContainer } from 'react-navigation' import { Colors } from '../themes' import Icon from 'react-native-vector-icons/FontAwesome' import ChatStack from './ChatStack' import ProfileScreen from '../containers/ProfileScreen' import TransactionStack from './TransactionStack' const chatIcon = ({ tintColor }) => ( <Icon name='comments' size={30} color={tintColor} /> ) const transactionsIcon = ({ tintColor }) => ( <Icon name='file-text' size={25} color={tintColor} /> ) const profileIcon = ({ tintColor }) => ( <Icon name='user' size={25} color={tintColor} /> ) const TabNavigator = createBottomTabNavigator({ CHAT: { screen: ChatStack, navigationOptions: { tabBarIcon: chatIcon } }, PROFILE: { screen: ProfileScreen, navigationOptions: { tabBarIcon: profileIcon } }, TRANSACTIONS: { screen: TransactionStack, navigationOptions: { tabBarIcon: transactionsIcon } } }, { tabBarOptions: { showIcon: true, showLabel: false, activeTintColor: Colors.purple, inactiveTintColor: Colors.gray } }) export default createAppContainer(TabNavigator)

Each of the above routes is a screen or stack of its own. They stacks are structured similarly to HomeStack above except that we use headers for some of the screens.

Individual Stack Components in Tab Navigator: ChatStack

The header items we use are defined in navigationOptions which is built into react-navigation.

I’ve created my own custom <BackArrow /> component for navigating back and passed it the navigation prop and all of the navigation actions that come with it.

The initial ChatListScreen doesn’t have a header, but because we defined a header for all of our screens in defaultNavigationOptions at the bottom, we needed to explicitly set this screen to not have one.

For the title in each header, I’m passing it the name of the person the user is corresponding with in the chat via navigation using navigation.getParam, and failing that, setting a backup default option. You can also just set it to a string.

file: OurApp/app/navigation/ChatStack.js

import { createStackNavigator } from 'react-navigation' import React from 'react' import BackArrow from './components/BackArrow' import ChatListScreen from '../containers/ChatListScreen' import ChatScreen from '../containers/ChatScreen' import ReviewInvoiceScreen from '../containers/ReviewInvoiceScreen' import PaymentFormScreen from '../containers/PaymentFormScreen' import CreateInvoiceScreen from '../containers/CreateInvoiceScreen' const ChatStack = createStackNavigator( { ChatListScreen: { screen: ChatListScreen, navigationOptions: { header: null } }, ChatScreen, ReviewInvoiceScreen, PaymentFormScreen, CreateInvoiceScreen }, { defaultNavigationOptions: ({ navigation }) => ({ initialRoutName: 'ChatListScreen', headerMode: 'float', headerLeft: <BackArrow navigation={navigation} />, title: navigation.getParam('correspondentName', 'Messages') }) } ) export default ChatStack

Individual Stack Components in Tab Navigator: TransactionStack

import { createStackNavigator } from 'react-navigation' import React from 'react' import TransactionsBackArrow from './components/TransactionsBackArrow' import TransactionsListScreen from '../containers/TransactionsListScreen' import TransactionScreen from '../containers/TransactionScreen' const TransactionStack = createStackNavigator( { TransactionsListScreen: { screen: TransactionsListScreen, navigationOptions: { header: null } }, TransactionScreen: { screen: TransactionScreen, navigationOptions: ({ navigation }) => ({ headerLeft: <TransactionsBackArrow navigation={navigation} /> }) } }, { initialRoutName: 'TransactionsListScreen', headerMode: 'float' } ) export default TransactionStack

Navigating between screens

In components, in order to explicitly move from screen to screen (as opposed to using headers to navigate back), use the navigation prop and the navigation action.

this.props.navigation.navigate('TransactionScreen')

This can commonly be called in an onPress() function.

Below you can see where the corresponding name is being passed through navigation to be used in ChatStack screen headers. You can also navigate to different stacks this way.

onPressChat = (correspondentName) => () => { this.props.navigation.navigate( 'ChatScreen', { correspondentName } ) }

So there you have it!

Just an example of the mixed bag of features we use in practice and a simple, yet robust navigation system for mobile apps.

Header photo by Hello I'm Nik on Unsplash