The intricacies of nesting navigators in React Native using react-navigation
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:
- Speeding up the build of a surprise dinner app in React Native using Expo
- Battle testing a ridesharing API and React Native’s MapView in Expo
- Supporting multiple languages in React Native with Expo Localization
- 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).
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.
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.
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 shouldComponentUpdate
and 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.