Routing and Navigation in React Native

Route-centric navigation with ExNavigator

James Ide
Exposition

--

Early 2017 update: Check out React Navigation, the routing and navigation library developed in collaboration between Exponent, Facebook, and other members of the React Native community. In some ways it is the spiritual grandchild of ExNavigator. We recommend you use React Navigation in your projects.

Fall 2016 update: Check out the new routing and navigation library, called ExNavigation. It supports several navigation interfaces like navigation bars and tabs, the back button on Android, and optimized transition animations that run on the native UI thread. ExNavigation also optionally works with Redux for more control over the route state and a more natural way to implement deep linking.

Navigating from screen to screen is a basic feature of most apps and touches a lot of surface area. To address some of the needs of real-world apps we built ExNavigator, a component that delegates to the Navigator that React Native includes. It is more opinionated and has the notion of an ExRoute, which defines how each screen looks and behaves. We use ExNavigator in production-quality projects and its development is guided by actual needs. This post explains the ideas that are in ExNavigator and how we use it so you can assess whether it is useful for your project, too.

Cohesive Routes

A natural way to think of your app is that it is made up of screens. Each screen has several parts: some content and optionally a navigation bar at the top with a title and buttons. A screen is also associated with a transition that says how to present its content, which could fly in from the right or pop up from the bottom, for example. All of these parts are related to each other since they make up a screen, and we want to think of them together when writing code.

An ExRoute object brings together the parts of a screen. This is a basic ExRoute:

The key property of ExRoute is that our code for rendering the screen’s content is near the code for rendering its navigation bar buttons and the code for configuring how it transitions. These parts of a screen are related and ExRoute increases cohesion by bringing them together.

Rendering a Route

ExNavigator takes ExRoute objects and tells Navigator what to render. Its API is similar to Navigator’s. You pass the ExRoute object as the initialRoute prop:

This renders the HomeScreen component with “Settings” and “Help” buttons in the navigation bar. The title of the navigation bar will read “Home”.

One convenient feature of ExNavigator is that it uses the title of the previous screen to render a back button by default. For example, if you were to push a profile screen on top of the home screen, the navigation bar would display “⟨ Home” in its top left. When you need to, you can customize the top-left button by defining renderBackButton in the home route or renderLeftButton in the new profile screen’s route (the latter takes precedence). Note that renderBackButton doesn’t refer to the current screen’s back button; it defines the button shown when another screen is on top of it. This concept is similar to UINavigationItem’s and is one way to customize the navigation bar.

Locally Customizing the Screen Content and Navigation Bar

Sometimes we need to parameterize the screen and the navigation bar. For example, a profile screen needs to know which user’s profile to display and the navigation bar should show the user’s name. To do this, we define a function that creates a route, and the arguments to this function are used to customize the screen:

To display a specific user’s profile we call this function with the user object. It returns a route for that user’s profile, which we display using the push method of the ExNavigator or its navigation context:

// Get references to "user" and "navigator" however you see fit
navigator.push(getProfileRoute(user));

This approach of passing in arbitrary arguments is quite flexible. We can pass in referrer data when a screen’s look depends on information from the previous screen. We also sometimes pass in an event emitter when the screen content and navigation bar buttons need to communicate. For example, on the profile screen there may be an “Edit” button in the top-right corner. When tapped, its text changes to “Done” and the profile screen enters editing mode. The button can inform the screen component using an event emitter:

The event emitter is a communication channel between the screen content and the navigation bar. It is an imperative channel instead of a more reactive program flow. In practice, though, the complexity of the imperative code is usually confined to the route’s components. Instead of an event emitter, you also could use a Flux-like pattern or Redux to update your navigation bar buttons and screen content in concert.

Navigating to New Screens

When the user taps a button and we need to navigate to a new screen, we need the new screen’s route and a reference to the ExNavigator component. We pass it down through the component hierarchy so our components can initiate navigations:

// For example, a component's event handler may go back one screen:
this.props.navigator.pop();
// Or go forward one screen:
this.props.navigator.push(getSettingsRoute());

It is also very common for screens to have their own navigator. For example, a settings screen that pops up from the bottom often has its own stack of screens as the user goes deeper into the settings. Use nested navigators when your active screens form a tree instead of a stack.

In fact, wrapping the entire app in a top-level navigator without a navigation bar allows you to present modal screens. React Native includes a Modal component, which is intended for hybrid apps that embed React Native under a native UINavigationController. When writing a pure-JS app, a top-level navigator is easier to work with. With a top-level navigator we can display modal screens in a cross-platform way (also, check out Jason Brown’s post for an in-depth walkthrough for displaying modals):

A screen can contain its own navigator, allowing for nested navigators. Pass the parent navigator to the child navigator to retain the relationship between them.

As demonstrated in the code above, be sure to pass a reference to the parent navigator down to the child navigator via the navigator prop. The parent navigator is assigned to a property of the child called parentNavigator. It is useful for dimissing the child navigator or modifying the parent’s navigation stack.

The React Native Navigator also has an object called the navigation context. It is similar to the Navigator component and has methods to push and pop routes, with a couple extra properties. Usually you don’t need to distinguish between the two and I recommend referring to the React Native source code on a need-to-do basis for the specific details.

Focus and Blur Events

ExNavigator will notify your ExRoute objects when a screen gains or loses focus. It will call the ExRoute object’s onWillFocus when a transition to the route begins and onDidFocus when the transition ends. Similarly, ExNavigator will call onWillBlur and onDidBlur on the ExRoute of the screen losing focus.

The focus and blur methods on an ExRoute object are called at the appropriate times. Other ExRoute methods are omitted from this example.

We use the focus and blur events to display or dismiss a keyboard when a screen is presented. ExNavigator uses the underlying Navigator’s “willfocus” and “didfocus” events, so refer to the React Native source code for the precise semantics. Be careful about how much work the “willX” listeners perform so that they don’t slow down the transition that is starting.

Performance

A high-quality mobile experience includes smooth, responsive navigator transitions. Specifically, they need to start quickly after the user initiates a navigation and the frame rate of the animation needs to be high and consistent. Currently React Native does not perform at the level we need it to, partly because each frame of the transition is calculated in real time in JavaScript. There are experimental avenues such as off-thread animations and incremental rendering that may allow React Native to in fact surpass traditional native apps. For today’s products, though, we mitigate performance issues by rendering the screen content after the transition is done.

We wrote a component called LoadingContainer to defer rendering. With LoadingContainer, a new screen smoothly slides in with a loading indicator. When the transition completes and there are no active interactions demanding high responsiveness (the navigator transition is considered an interaction), we update our Redux data stores and render the new screen’s actual content. The end effect is reasonably pleasing when running a release build without debug checks and especially without the latency of the Chrome debugger, but we would like for React Native to be fast enough to render screens without deferring them.

URLs

For basic applications with a navigation stack instead of a tree, we can handle URL-based navigation by converting URLs to ExRoute objects. How you do this is up to you and the concept is basic. Likewise, we convert ExRoute objects to URLs to serialize the user’s navigation state.

URLs are converted to ExRoute objects that are rendered by the ExNavigator

One aspect of apps — desktop or mobile — that is harder than on websites is that routes often imply context. When I say that routes imply context, I mean that when a user directly navigates to a profile screen via a push notification, they expect the profile to be above a home screen they can go back to; the profile screen implies that the route stack also contains the home route. In contrast, on the web there is no expectation nor desire that directly visiting “example.com/@user” will make your browser’s back button go to “example.com/home” unless the user was previously on that page anyway. One way to implement implied context with React Native is to call immediatelyResetRouteStack to render the current route as well as the implied ones beneath it.

Ultimately, a solution that borrows some of the ideas from React Router could be appealing. React Router provides great support for URLs. And we want great support for URLs because it becomes trivial to support deep linking for push notifications, inter-app links, Apple’s universal links, and so on. We eventually would like to build “ExRouter” to handle the various needs of apps.

Navigating the Future

ExNavigator and React Native’s Navigator are constantly evolving. The APIs are changing to accommodate use cases as we understand them better. As an example, features like “hierarchical navigation” would improve nested navigators by letting navigators automatically bubble up routes that they don’t know how to handle. A more React-like API could replace Navigator’s imperative push and pop calls. Simultaneous interoperability with Redux and Relay is something we’d like to support in ExNavigator. These are some of our ideas for the future of navigation and routing and by building real apps we’ll get there.

Special thanks to Brent Vatne for proofreading a draft of this post.

If you liked this post, recommend it to others by clicking the button below. Follow @exponentjs on Twitter and join the Exponent community for more high-quality discussion.

--

--