The intricacies of nesting navigators in React Native using react-navigation

Photo by Andrew Neel on Unsplash

This is the last post in a 4-part series about building the iOS and Android app for uncovercity using React Native. You can find the other ones here:

  1. Speeding up the build of a surprise dinner app in React Native using Expo
  2. Battle testing a ridesharing API and React Native’s MapView in Expo
  3. Supporting multiple languages in React Native with Expo Localization
  4. The intricacies of nesting navigators in React Native using react-navigation

In one of my most recent projects, I built the native app for Spanish startup uncovercity using React Native.

One of the most interesting challenges I faced was setting up the navigational structure based on a set of Invision designs. In this post, I’m going to talk about them in detail and walk you through the solutions I came up with.

It all starts with a tree

The following bullet points illustrate how the app was structured. I’m already giving you a little hint on which type of navigator I used for each element that contains child routes.

  • Main (StackNavigator)
    • Home
    • Login
    • Recover password
    • Sidebar (DrawerNavigator)
    • Router (StackNavigator)
      • Experience
      • Experience Map
      • Experience Active
      • Experience Active Detail
      • Map
      • Evaluation
    • PastExperiences
    • FAQ
    • Contact

Let me walk you through this tree step by step.

Level 1: Stack Navigator — the welcome and login screen

As you can see, the root element is a StackNavigator. Since the first three screens (Home, Login, Recover password) just show up on top of each other as you move from one to the next, we can use a simple StackNavigator which comes with a nice default transition (slide-to-left on iOS, slide-to-top on Android).

The Stack Navigator in action

Level 2: Drawer Navigator — the sidebar

The next component serves as a container for a bunch of other routes. We’re using DrawerNavigator here so that the user can slide in a sidebar menu from the left using touch gestures or the controversial hamburger icon in the top left corner of the screen.

From the sidebar, the user can get quick access to some static pages like FAQ and Contact. But by default, it displays another container element which contains a couple of other sub-routes, the Router.

The Stack Navigator in action

Level 3: Stack Navigator — the router

The router is the heart of the app and contains most of the content. It’s where the user can see the current status of her (dinner) experience, a screen with detailed info about the venue, a map with the pickup/return point and an evaluation screen that lets her rate the experience when it’s over.

This again is a StackNavigator because we want to use the default transition on iOS and Android. But the router also contains some additional startup logic that I’ll cover in more detail below.

The Stack Navigator in action

Challenges I faced using react-navigation

React Navigation simplifies a lot of things that are hard to achieve in native iOS and Android. But it also comes with its own set of issues whose solution or workaround is not always straightforward.

What follows are the different issues I ran into and how I solved them.

Challenge #1: enabling header bars inside a DrawerNavigator

The StackNavigator has a header bar which is enabled by default. It can be disabled using headerMode: none on the StackNavigator’s visual options. This is desirable when you want the current screen to cover the entire viewport, including the space that the header usually occupies.

What’s not so intuitive is when you’re inside a DrawerNavigator and want to enable the header bar for all its children. The DrawerNavigator doesn’t provide an inheritable headerMode option like StackNavigator since its purpose is not to give you a navigable stack of screens, but a flat hierarchy that contains screens with no header by default.

To enable the header bar for those screens as well, you need to wrap each of them in an additional StackNavigator. This brings back the header bar for each of its child screens, even though we only have one and actually abuse the StackNavigator for this simple purpose. While this seems like a hack, it still seems to be the most common workaround in most situations.

const One = StackNavigator(
  {
    main: { screen: Main }, // only has one route
  },
  {
    navigationOptions: {
      headerMode: 'screen', // enabling header mode for main screen
    },
  },
)

const AppNavigator = DrawerNavigator({
  one: { screen: One },
  two: { screen: Two },
})

Challenge #2: toggling the sidebar from within nested routes

Next, I was wondering how to toggle the DrawerNavigator’s sidebar from a component inside a nested route.

Since React Navigation’s 2.0 update you can use the openDrawer function on the navigation property. But that property isn’t available in each component unless you pass it down as a prop, which sometimes means passing it down a long component chain.

A neater solution (spotted in react-navigation’s documentation) is to store the DrawerNavigator’s reference in a service and importing it wherever you need access to the openDrawer function. This is what the service’s methods do:

function setTopLevelNavigator(navigatorRef) {
  navigator = navigatorRef
}

function openDrawer(routeName, params) {
  navigator.dispatch(DrawerActions.openDrawer())
}

The component that renders the DrawerNavigator then has to pass the navigator reference to the setTopLevelNavigator function.

<TopLevelNavigator
  ref={(navigatorRef) => {
    NavigationService.setTopLevelNavigator(navigatorRef)
  }}
/>

Now you can simply call the navigate function from anywhere to have access to the DrawerNavigator 's openDrawer method:

NavigationService.openDrawer();

You could probably accomplish the same using React’s Context API (introduced in React 16.3) or using Redux or Mobx, but at the time of writing this code, this seemed to be the most simple solution without introducing any overhead.

Challenge #3: taking authenticated users to the user area

If you’re a registered user and have already logged into the app before, you obviously want to be treated as a logged-in user the next time you come back.

Coming back to the navigation tree outlined above, that would mean taking her from the root navigator (Main) to one of the screens nested inside the Router stack. Quite a leap!

Main -> Sidebar -> Router -> Experience -> Login

In Main, we grab the users current status from the phone’s local storage (AsyncStorage). If the user is logged, it will contain an object with the user’s details (ID, email, name) and we take her to the next level, Sidebar. If she is not logged in, she’ll be taken to the Login screen instead.

const user = await AsyncStorage.getItem('user')

Since it’s an async function, we need to wait for it to complete before rendering the outer navigator, so we return a loading screen in our render function and we only set loading to true once our AsyncStorage operation has completed.

if (this.state.loading) {
  return <LoadingScreen />
}

In the navigationOptions of our second-level navigator (Sidebar) we set the initialRouteName to the next screen in our navigation hierarchy depending on the user’s login status.

const Navigator = createStackNavigator({
   ...routesObject...
  },
    {
      initialRouteName: (this.props.loggedIn) ? 'Sidebar' : 'Login',
    },
});

In the Sidebar, we then move on to the initial route Router, whose siblings can be accessed via the DrawerNavigator.

const Drawer = createDrawerNavigator({
  Router: { screen: Router }, // another StackNavigator in here
  PastExperiences: { screen: PastExperiencesScreen },
  FAQ: { screen: FaqScreen },
  Contact: { ContactScreen },
})

Inside the router however, the screen we show to the user depends on the current status of her experience. The experience details are part of a subsequent API request that only resolves after the user has logged in.

This led to another few routing issues that I had to work around and which I’m going to cover next.

Challenge #4: showing a nested screen based on an API response without screwing up the user experience

Last but not least, I had to solve an issue that was related to how the app interacts with the API. Inside the Router, the screen that the user sees is based on an API response from a ride-sharing service.

The user sees one of the following: pickup, return, map or evaluation, based on the current status of the ride that takes her to the restaurant and back.

Main -> Sidebar -> Router -> Pickup screen -> Map -> Return screen -> Evaluation

This sounds easy in theory, but there were three things I had to take care of to prevent the app’s UX from feeling awkward and confusing:

  • first, I had to only render the initial screen once. Since we relied on polling to retrieve API updates, every subsequent update should not re-render the router, or it would result in a really annoying flicker effect.
  • second, I had to make sure to obtain the ride-sharing update before calling the initial route or opening the app would always result in a transition from the initial (pickup) to the current screen (map, return or evaluation).
  • third, I had to take into account that users might background the app for a while and once they come back to the app an update of the current screen should be triggered in case anything changed in the meantime.

For the first issue, I used shouldComponentUpdateand wrote some logic that only updated the screen if the actual route changed. This helped me get rid of the flicker effect, even though I recently learned that shouldComponentUpdate should actually not be abused for this kind of purpose.

For issue number two, I had to request a little API change that would return the status of the user in the initial response from the server (one that wasn’t initially designed for that purpose). But I also had to make sure the screen wouldn’t show up before evaluating the response. So I had to use the technique I already used in Challenge 2, returning a LoadingScreen as long as other logic is being executed in the component lifecycle.

For those living on the cutting-edge of technology, React Suspense is going to help with that kind of async rendering in the future.

For the third issue (by far the trickiest), I had to wrap the router inside another React component to make sure an update occurs after the app has been backgrounded.

Because my previously mentioned prevention logic (shouldComponentUpdate) also kept users stuck in the current screen, I had to make sure the component containing the router renders a new router whenever an update occurs.

This is roughly what the stateless functional component looks like. It creates a new StackNavigator under two conditions:

  • the initialRouteName has changed
  • no RouterStack has been defined before.
let RouterStack
let lastRouteName

const RouterStackWithInitialRoute = ({ initialRouteName }) => {
  if (RouterStack === null || lastRouteName !== initialRouteName) {
    lastRouteName = initialRouteName
    RouterStack = createStackNavigator(
      require('../../../constants/routeConfig').default,
      {
        initialRouteName,
      },
    )
  }
  return <RouterStack />
}

Passing the initial route name into that component looks as follows:

render() {
  return this.state.loading ?
    <LoadingScreen /> :
    <RouterStackWithInitialRoute
      initialRouteName={this.state.initialRouteName} />;
}

Now every time the user comes back to the app, she’ll immediately see the screen that corresponds to the current status of her experience.

Please keep in mind that all code examples are just a simplification of the actual code to illustrate the point I want to make. So no guarantee that they are going to work in case you try to copy/paste any of them :)

Takeaways from building a seemingly simple mobile app in React Native

That’s it for my series on building the mobile app for uncovercity.

I have to admit that judging by the initial user journey I wouldn’t have anticipated such a high level of complexity for something that seems so simple on the surface. And I probably didn’t always come up with the most elegant solution for each of these problems either.

But taking into account all the possible user scenarios and API issues that surfaced during the project under real-life conditions, it’s very easy to underestimate the effort that needs to go into a project like this.

The actual work that goes into shipping a battle-tested, bug-free mobile app can grow exponentially if you have to deal with third-party APIs, community-driven libraries and a cutting edge technology like React Native.

If you happen to be in Spain, you can find the app in the iOS and Google app stores. Just make sure to book a table on to get the full app experience :)

Originally published on Medium on December 3, 2018.