pact merging flow React Navigation has become a standard in navigating between screens in the React Native. There are just four basic navigators, with an option to create a custom one, but the magic happens when you combine them in the right way.1

Stack navigator

  • transitions between screens
  • navigation history on stack
  • built-in header

Bottom tab navigator

  • no navigation history
  • buggy API for returning active tab
  • highly customizable bottom tab

Switch navigator

  • no additional UI
  • automatic screen unmount on leave
  • returns to initial screen on back button press (no navigation history)

We’ll skip the Drawer Navigator to use the Tab Navigator Instead.

First navigator

SwitchNavigator will be the type of the root navigator. Add Splash screen which automatically redirects to the Launch screen and also write SignUpName and SignUpVerifyEmail with simple buttons that change screens.

const PrimaryNav = createSwitchNavigator(
  {
    Splash: SplashScreen,
    Launch: LaunchScreen,
    SignUpName: SignUpNameScreen,
    SignUpVerifyEmail: SignUpVerifyEmailScreen
  },
  {
    initialRouteName: 'Splash',
    backBehavior: 'initialRoute'
  }
)

export default PrimaryNav

Result

Tab navigation

Inside SwitchNavigator, include TabNav with its 3 screens. It’s important to do it directly here and not to render it like <TabNav /> to keep navigation state in one place and avoid denying access to the screens in incorrectly rendered navigation from the PrimaryNav screens.

const TabNav = createBottomTabNavigator({
  Home: HomeScreen,
  Notifications: NotificationsScreen,
  Info: InfoScreen
})

const PrimaryNav = createSwitchNavigator(
  {
    MainNav: TabNav,
    Splash: SplashScreen,
    Launch: LaunchScreen,
    SignUpName: SignUpNameScreen,
    SignUpVerifyEmail: SignUpVerifyEmailScreen
  },
  ...

Result

Nested stack navigator

Next, add separate Stack Navigators for each tab. Because it includes a header by default and we already built one, create invisibleHeader object to unpack it in the config.

const invisibleHeader = {
  headerMode: 'none',
  navigationOptions: {
    headerVisible: false
  }
}

Stack navigators:

const HomeScreenNav = createStackNavigator(
  {
    Home: HomeScreen
  },
  {
    ...invisibleHeader,
    initialRouteName: 'Home'
  }
)

const NotificationsScreenNav = createStackNavigator(
  {
    Notifications: NotificationsScreen,
    Notification: NotificationScreen
  },
  {
    ...invisibleHeader,
    initialRouteName: 'Notifications'
  }
)

const InfoScreenNav = createStackNavigator(
  {
    Info: InfoScreen
  },
  {
    ...invisibleHeader,
    initialRouteName: 'Info'
  }
)

And modifiy the PrimaryNav to replace screens with new navigators

const PrimaryNav = createSwitchNavigator(
  {
    MainNav: TabNav,
    Splash: SplashScreen,
    Launch: LaunchScreen,
    SignUpName: SignUpNameScreen,
    SignUpVerifyEmail: SignUpVerifyEmailScreen
  },
  ...

Result

Hide tab bar when going deep

Change bottom Tab config to hide the TabBar when switching away from the initial screen of each Stack Navigator.

for (let nav of [HomeScreenNav, NotificationsScreenNav, InfoScreenNav]) {
  nav.navigationOptions = ({ navigation }) => ({
    tabBarVisible: navigation.state.index <= 0
  })
}

Result

Common screens in stack

We want to have access to some common screens in multiple places, without compromising the history integrity. The idea is to always keep navigators in a straight tree to prevent switching up and down the tree between navigators (branches) when we need to access common screens (actually any given screen). To do so we simply duplicate common screens in each Stack Navigator. We keep it in a separate object:

const commonScreens = { Settings: SettingsScreen }

Next, add prefixes to each common screen depending on the Stack Navigator initial screen name. If you are lazy like me you can use ready-made frunctions from Ramda and Ramda-adjunct (my go-to utils libraries, like lodash but fully functional and never mutating input):

import * as R from 'ramda'
import { renameKeysWith } from 'ramda-adjunct'

const generateCommonScreens = prefix =>
  renameKeysWith(R.concat(prefix), commonScreens)

Or just write your own. It’s simply adding a prefix to each object key.

Then also unpack it to each Stack Navigator like so:

const HomeScreenNav = createStackNavigator(
  {
    ...generateCommonScreens('Home'),
    Home: HomeScreen
  },
  ...

After it navigate to whatever common screen we want and pass according screen name depending on what tab it has to navigate from, e.g. HomeSettings and InfoSettings.

Result

Custom back button behavior

There is no reliable built-in way to get the current screen in TabNavigator. To customize the back button behavior, create a custom container to wrap screens with.

import React, { Component } from 'react'
import { withNavigation } from 'react-navigation'
import { BackHandler } from 'react-native'

class HandleBack extends Component {
  constructor(props) {
    super(props)
    const { navigation } = this.props
    this.state = {
      active: false
    }
    this.didFocus = navigation.addListener('didFocus', payload => {
      this.setState({ active: true })
    })
    this.didBlur = navigation.addListener('didBlur', payload => {
      this.setState({ active: false })
    })
    BackHandler.addEventListener('hardwareBackPress', this.onBack)
  }

  componentWillUnmount() {
    this.didFocus.remove()
    this.didBlur.remove()
    BackHandler.removeEventListener('hardwareBackPress', this.onBack)
  }

  onBack = () => {
    if (this.state.active) {
      BackHandler.exitApp()
      return true
    }
    return false
  }

  render() {
    return this.props.children
  }
}

export default withNavigation(HandleBack)

We have to manually save whether the current tab is active or not. Explicit cleanup of event listeners is also a good practice. Lastly, wrap the Home Screen with this component in the render function.

Result

Updated:

Leave a comment