Note: this is part of my upcoming Programming React Native book. Enjoy!
You might ask yourself: why do you need navigation? What’s a navigation stack? Well, navigation on mobile apps covers these concerns:
- Situational awareness: know where you’re at, and possibly, where you came from
- Functional: be able to navigate. Go back, undo, skip, or “deep link” into some functionality of the app
- Infrastructural: consolidate ceremonies that need to be done before rendering a new screen.
- Maintainability: to realize the above concern, often, you’ll need to build a state machine. A navigation stack is a state machine. Platforms may let you code this state machine declaratively or visually, and promote maintainability by keeping all of that logic in one place.
Why Navigation is Scary
In my opinion, one of the scariest topics in mobile application development is routing and navigation.
Navigation infrastructure typically holds a navigation stack, which holds screens, which hold components and data. Often, this state is not represented anywhere; it is a transient state that the user have created by merely navigating around, which is why it makes it hard to reason about.
Navigation flows are also hard to reproduce, should you bump into bugs in that area, and often these bugs carry memory leaks and the sorts (we did just mention a navigator stack holds reference to screens, which hold reference to components, which holds reference to data, and so on).
Navigation UX deals with how each operating system brought with it its own truth as to how to properly do navigation, as well as how to facilitate it: Android’s implicit back button behavior, and iOS with its purely explicit text navigation bar.
And lastly, each platform has its own tooling and how to ease up all said pains. Android has the traditional approach, where you build screens and code transitions with the omnipotent
Intent, and lately iOS offered a more intuitive approach with Storyboards and almost physically connecting screens.
Navigation in React Native
The bad news is that navigation doesn’t get less scary to me, even with React Native. I guess that for me the saying “the more you know, the more you worry” painfully applies.
- React sports one-way binding, which helps us reason about our code. So to find memory leaks we travel a one-directional dependency tree (YMMV).
- React Native gives us plenty of escape hatches. This is the case where we want to use
NavigatorIOS, the native iOS navigation stack, in order to minimize the element of surprise by not emulating a navigation stack, or simply put – sticking to the host OS best practices.
In addition, if what you’re building is really painfully complex (years of effort and complexity), I guess at this stage you probably don’t really need this book and should build your app on each platform separately using dedicated tooling and approaches, just to be safe.
Let’s take a look at the two React Native navigation solutions.
Navigator vs. NavigatorIOS
React Native started on iOS, and this is why the best-in-class or perhaps most used navigation stack is
NavigatorIOS. If you’re building an iOS only app and want to play it safe, use it. However, if a year or more passed from the writing of the book, it might be possible that the generic
NavigatorIOS in features and stability, so allow yourself to evaluate that again by Googling around.
Navigator is React Native’s attempt to abstract the concept of navigation. At the time of writing, it is good-but-not-great a bet for a smooth ride if you want to keep a common navigation codebase for iOS and Android.
NavigatorIOS for iOS and
Navigator for Android. Even if reality changes and
Navigator becomes the be-all-end-all navigation solution for both platforms, I would want to keep it apart. It feels that navigation on iOS and Android will always want to be different, somehow, either UX or hardware-wise, so it makes sense to future-proof this and make room for code to evolve differently.
Let’s go through a brief
Navigator example. For this, we’ll assume the common master-detail pattern, where we have a master view containing a list of items, and then when tapping an item we want to navigate to a child (or detail) item.
This is what you probably wanted to read before I filled your head with back button craze.
The Navigator is a React component that deals with two main concerns:
- Returning a properly configured
Navigatorcomponent, with icon, back icon, colors, initial route and more. In addition, it should wire a
renderScenehandler, which we talk about next.
Here is how we set that up, for a simple two-screen (“first” to “second”) navigation flow:
render function, we set up a
Navigator, in which the most important properties to specify are the
id property; if we have anything else to pass, we’ll use a suitable payload, within the same object. However, let’s agree that
id will always be reserved for routing.
The good news is that we passed the clunky part of declaring a
Navigatorcomponent and then wiring up screens. It can become less clunky, maybe, some day. It can have some sort of a DSLish feel to it such as:
But this kind of experience doesn’t exist yet. It’s quite a low-hanging fruit, so think of it as a nice weekend project to do 🙂
Next up, our screens are really normal views that take care of massaging a
ToolbarAndroid to their needs. A
ToolbarAndroid is our top navigation bar, and with React Native, it is quite flexible. So flexible, in fact, that we need to code every decision point such as when to show the back icon, what kind of title to display and so on, based on our current and past screens.
ToolbarAndroid is also the real Android
Toolbar widget (see here) which, in accordance to our theme here (showing the platform-specific way first) is an Android-specific component.
Flexibility can be good and bad. Good since on each screen, we can specify exactly how we want things to be had on the navbar. Bad, because we need to be defensive here since it can become a maintenance overhead or a state machine of the worst kind – the one that is spread out through out our entire codebase.
Here is our
If you’re trying this out, make sure that
ic_arrow_back_white_24dp is an icon you’ve dropped in your resources folder – in this case Android. For the sake of the experiment you can use a single hi-res image for all size variants.
Note here, that we specify our navIcon explicitly. We want users to be able to tap a back icon right on the navbar.
Next up, our
Here, we don’t specify a back button, because we recognize it as the root screen. The most important (and fun) piece of code here is how we navigate to the
Second screen. We just
push a new object which happens to contain an
id that we agreed is our routing property that we base our routing on. Note that we don’t specify the screen type, object, or tag here – this is the essence of separating routing from destination or implementation, which is a Good Thing.
This completes our vanilla
Navigator walkthrough. Next up we’ll take a look at its older brother:
On Android, we’ll have to take care of navigation, and also the back button.
Android’s Back Button
Tapping into the back button is unique to Android devices, in that it is on occasion a “hardware” button on a dedicated touch area of the glass as in Samsung phones or HTC phones, or a software button that renders at the lower part of the screen (common on the Nexus family of devices).
The following boilerplate is more or less needed verbatim in our app to support responding to the back button. Note that the
_navigator variable is scope-global, and it gets filled on first navigation. First read this snippet of code to understand what’s going on, and then I’d recommend tracking the
_navigatorvariable throughout as well.
BackAndroid is a simple library that binds to the native events of the host device.
Deep dive alert!: here’s how it works (notice the special exitApp case):
On iOS you need not worry if you’re mistakenly (or if you lazily want to use the exact same code) using the
BackAndroid module – all of the functions it carries are no-ops.
As with any event listener, when you add a listener, you must immediately ask yourself how you are going to remove it – does the
subscribemethod return a special handle you need to provide when canceling?, or do you have to do the bookkeeping yourself and come up with the same reference to the handler you provided?
BackAndroid case, when we subscribe we must keep a reference to the handler function we give it. However, do we really see ourselves disabling the back button in real life?
ToolbarAndroid and Navigator.NavigationBar
It is important to notice that
Navigator comes with a pre-baked piece of UI that functions as your top Android navigation bar. If you’ll dig into UIExplorer within the
facebook/react-native Github repo, you’ll see these kinds of things:
That is a concept of a
NavigationBarRouteMapper or in other words, you’re telling your
Navigator how to compose the
NavigationBar’s UI on each and every route.
Then, you stick an actual
NavigationBar into your
Navigator this way:
So what you’ve got here, is a
Navigator which we learned how to configure and tell it how to render itself as a response to a given route, and a
NavigationBarthat pretty much does the same.
Immediately, you start thinking – why separate the two? Your
Navigatorrendering is spread out through two different mappers, and also, why not make a route a first-class citizen and making itself render? something like ShowCartRoute.render(), and then ShowCartRoute.renderBar()? well, this is what Exponentjs/ex-navigator solves exactly, and more or less feels like. I recommend checking it out. Also, you might imagine that this is not going to be the only flavor people would like to do their routing with, so keep watching out for new things.
Should you use a
ToolbarAndroid or a
NavigationBar? The answer is again – tradeoff. The
NavigationBar is strongly tied to routes,
ToolbarAndroid is tied to your view.
ToolbarAndroid is, well, Android, and you could probably implement such a thing yourself generically.
So bottom line, if you’re implementing something simple, then go with iOS’s
NavigatorIOS for iOS (coming in the next section), and
ToolbarAndroid for Android. Otherwise, use a
Navigator for both, and either
NavigationBar, or your own piece of bar that you construct manually. And, of course, do a quick browse on my awesome-react-native list for any fancier bar that you’d like.
This navigator is a bit simpler, and relies on the native iOS navigation stack. The iOS navigation stack is a powerful beast, it is only encouraging that it is hidden under such a simple React Native component, however be sure that if we wanted to do more involved things (custom Segues and such) it might have become trickier.
For now, let’s implement the same
Navigator example with
Navigation component is simple and explicit, and the initial route specifies the verbatim component (here
First) that we want to run. As a side note, you might also like to call this component
Handler or anything that represents a concept of a routing shell component.
Next, we take a look at both our screens,
Somewhat similar in structure to
Navigator, however again we see explicit mention of the
Note that when we use a
NavigatorIOSand a plain child
Viewas in this case, we will need to handle the height of a typical iOS navigation bar. In other words, we need to add a
paddingTopproperty to our
Viewsuch that the content will be offset from under the navigation bar. On more advanced components, such as the
ScrollView, we might want to look for the
automaticallyAdjustContentInsetsproperty that allows the component to handle this for us automatically.
Second screen looks at least as simple:
As mentioned during the
NavigatorIOS is simpler – we don’t have any navigation bar to tweak here, and in this case the view is completely vanilla – reusable and clean as-is.
Often when doing navigation, in addition to the route, we want to pass an additional data, for the new screen-to-be to root on.
In the generic
data property), so we can do something like this:
NavigationBar it will mostly be the same, if you take care to pass your data within your
routeMapper route object, and picking it apart in your
NavigatorIOS we need to use the special
And the receiving component will get both the
data property and a special
navigator props it can use to make decisions and to navigate further.
There is a fascinating question about when a new screen is born due to a new navigation. Is the data that the screen just got from the route a pointer to data it needs to fetch?, or the actual data it needed to fetch verbatim?. No answer is wrong: it is a trade-off between being implicit and explicit, or defensive rather than naive, respectively.
Search in Navbar
NavigatorIOS– Can’t do this.
Navigator.NavigationBar– make sure that your NavigationBar mapper will render your variant of a title like this:
ToolbarAndroid– make sure that the screen you are routing to, which is supposed to contain the search bar inlined within the navbar, should now render a single child which is your searchbar, prefer not providing a title in this case (styling and others are cut for brevity):
A searchbar is interactive – it is completely OK to make the
AwesomeSearchbarcomponent interactive by supplying callbacks via
props. You can pull these callbacks in two ways:
NavigatorBarrenderer – from your route, or by making the renderer take parameters.
ToolbarAndroid– since it is rendered within a container view, simply the callbacks of the neareset smart component will do.
Custom Content in Title
As in the previous pattern, the idea is similar sans the callbacks.
NavigatorIOS– content is string only
ToolbarAndroid– as with the previous pattern, simply hand over the component you want to render, this time no callbacks or interactivity needed.
Routed Navbar Content
We saw this while handling
NavigatorBar mapper – the content can change as a function of the route you are not traveling into.
NavigatorIOS– each time you
pusha navigation, you can define how the coming-to-be navbar will look like, so this is a simple matter of using a new title:
NavigationBar– use the route mapper, as we’ve seen before.
ToolbarAndroid– this is trivial since you’re rendering it as part of the view, so you can couple the rendering to the view itself. Meaning, don’t fuss with the route, but with the actual view component content.
Reactive Navbar Content
There comes a time where your navbar changes, but not as a reaction to route changes, but to some kind of an external event, timed event, or anything reactive in nature.
The way to solve it right now, which is gaining consensus, is to inject an event emitter to your app flows, and make sure that components on that navbar know to use it.
NavigatorIOS– again, not possible
ToolbarAndroid– make sure that the content you give each, will be able to use your global event emitter:
You can also not use an explicit emitter but a Flux dispatcher, or do use an explicit emitter and inject it via something similar to React’s context, an so on. Since this is an advanced/thought material topic (under the “lightbulb” category) I’ll leave it to you for exploration.
If you want to just go to a screen to collect input (i.e. modals), you can either use the actual
Modal component (see here), or send off a view with a callback within the
renderScene logic block. At extreme cases you can use event emitter, or if you’re using Flux than a Flux action trivially solves it.
If we scratch our head for a moment, we remember that routing in
And this way we can then do something like this, within our
Using the new spread operator
..., we easily inline the entire
props bag of properties into our component. Remember React will go left-to-right on various props so you can enjoy a cascading effect (provide defaults and then override them with specific data)
This concludes our discussion about routing. In summary, we’ve learned the following:
- Routing is hard, however at least on mobile, we have less rope to hang ourselves with by adopting the best practices of each mobile platform.
- React Native offers the generic and flexible
Navigatorfor a general case use, and the older
NavigatorIOSfor iOS specific work. Choose the latter if you don’t need flexibility and want to run fast by getting the default iOS navigation stack behavior.
- Routing is either defined implicitly with
Navigatorand routes, or explicitly with
NavigatorIOSand the specific components we route to.
- Remember to compensate for various UI glitches when using a navbar. Some content may vanish under the navbar because it is not padded.
- Passing data is easy through both
NavigationBaris a common to both concept, it has its own tradeoffs.