React Native Example App: Navigation

At TaskRabbit, we have been looking into building an iOS application in React Native. This is probably the first pure technology that I’ve been this excited about since moving from C to Ruby.

However, it’s definitely still in its early days. There are not many examples of how people are doing things out there. To help remedy this and share what we are learning, I made a sample React Native application.

The app itself is vaguely like twitter and/or tumblr. There are users that make posts. They follow other users. You can look at users they follow follows and those users’ posts. And on and on! The features (or styling) isn’t the main point. At this time, we’re mostly demonstrating architectural concepts.

The app we’re working on is a bit ahead of this one, but I think it will be neat to have this one publicly walk through the same steps that we have done privately. Everything is pretty new and the patterns are not established. We’ll post here about some pattern or refactor and update the app and hopefully start a great conversation.

Navigation

The first pattern that I wanted to talk about is navigation. The web has a pretty solid navigation story (with the URLs and such) and some tools to map that to React applications. I think it’s less clear on the phone.

I’m not sure why it’s different, actually, because there is still usually a “Back” button and one screen at a time. Yet, iOS development seems to have evolved in another direction. Most apps are all about this NavigationController and we push and pop and stuff like that. Then things get totally weird when we try to put the URLs back in for something like deep linking.

React Native supports the same concept with the NavigatorIOS or the Navigator. We went with Navigator because it was more customizable and would like work better cross-platform. But it still had this push and pop incremental mindset that, otherwise, React totally removes because we’re always rendering the whole thing based on state and props.

We also knew that we needed to much better at deep linking. So I decided to make URLs into a first-class citizen. The only way to show anything on the app would be to set a URL. This means that whatever is showing on the screen and where the back button goes is all dictated by the URL.

Router

How does this work? For example, sample://dashboard/posts is the first screen when you log in. You can toggle that to sample://dashboard/follows to see who you are following. If you tap on “john” there, you are now at sample://dashboard/follows/john/posts and you see his posts. If you tap the “back” button, you’re back on sample://dashboard/follows.

To make this work, we have to parse these URLs and determine the routeStack of the application to give to the Navigator. So for example sample://dashboard/follows would map to a single item in the stack:

1
2
3
4
5
6
[{
  component: require('../Screens/FollowList'),
  passProps: {
    username: username // null for current user
  }
}]

and sample://dashboard/follows/john/posts would map to the new screen along with the original.

1
2
3
4
5
6
7
8
9
10
11
[{
  component: require('../Screens/FollowList'),
  passProps: {
    username: username
  }
}, {
  component: require('../Screens/PostList'),
  passProps: {
    username: username
  }
}]

Creating this stack is the job of the Router and the Routes. The root of the app gets it’s state set with the routeStack. That’s given to a Navigator with it set as the initialRouteStack. As the state continues to change, it is set on that Navigator, who takes care of showing the current view and knowing what “back” is.

To do this, the Router basically just divides the url up into pieces (separated by slash) and iterates through calling parse on the previous result. It’s not beautiful, but it works.

I’d love to figure out how to extract the Router into it’s own library or use another one, but I haven’t had the time to check it out. One requirement that might be a problem is that it needs to be infinitely recursive. If it’s going to be responsible for the entire navigator state, then it has to support infinitely long urls like when I look at someone’s profile and then the users they follow and then pick one and then users they follow and then that user’s posts. It’s not clear to me if any web-based solution solves that.

Here what I mean by recursive. This is sample://dashboard/follows/jrichardlai/follows/taskrabbit/follows/bleonard/follows/taskrabbit/follows/bleonard/follows/jrichardlai/post (and back):

Actions

How did that state get set in the first place? There is an AppActions class with a few ways to manipulate the current state.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
var AppActions = {
  // launch given known url
  launchRoutePath: function(routePath) {
    Dispatcher.dispatch({
      actionType: AppConstants.LAUNCH_ROUTE_PATH,
      routePath: routePath
    });
  },

  // launch given current and relative url
  launchRelativeItem: function(currentRoute, item) {
    var navItem = assign({}, item); // clone so we can mess with it

    if(!navItem.routePath && navItem.replacePath) {
      var pieces = currentRoute.routePath.split("/");
      pieces[pieces.length-1] = navItem.replacePath;
      navItem.routePath = pieces.join('/');
    }
    if(!navItem.routePath && navItem.subPath) {
      navItem.routePath = currentRoute.routePath + "/" + navItem.subPath;
    }
    navItem.currentRoute = currentRoute;
    this.launchItem(navItem);
  },

  // go back
  goBack: function(navigator) {
    var current  = navigator.getCurrentRoutes();
    var previous = current[0];
    if (current.length > 2) {
      previous = current[current.length-2];
    }
    AppActions.launchRoutePath(previous.routePath);
  }

These actions can be called when the user taps an item in the list, the back button, or whatever. It dispatches the event, it’s picked up, and the root state is changed.

Navigator

One great thing about iOS and it’s traditional NavigationController pattern is the animations. When you tap on that item in the list, it slides in from the right. When you hit the “back” button, it slides out. Launching URLs old-school web-style doesn’t do that at all. It just pops in. Lame.

Fortunately, we can get the best of both worlds. The routeStack is given to the component as a prop, so it triggers the componentDidUpdate lifecyle method. In this method, if we just did this.refs.navigator.immediatelyResetRouteStack(this.props.routeStack.path) then it would be abrupt like the web.

However, we can also look at the new stack and the previous one and be smart about it. It’s a bit crazy looking, but it basically only ends up doing that if it’s completely different. We can handle the most common cases intelligently:

  • this.refs.navigator.push(nextRoute) if the new route is one item added to the previous route
  • this.refs.navigator.pop() if the new route is one item removed from the previous route
  • this.refs.navigator.replace(nextRoute) if the new route is a peer of the previous route
  • otherwise reset the whole stack

So in this one spot, we say it’s ok to be not quite as stateful. It gives people the experience they expect and everywhere else gets to treat the world as fully URL-driven.

Navigation Benefits

All of this really helps with deep linking or putting the app in the correct state based on a push notification. We had tons of issues before to be able to reconfigure the app (and it’s navigation stack) when the notification is slid over. Now it’s just calling AppActions.launchRoutePath() and the URL can be sent within the push itself.

We also know/require that every single screen (display of a route) has to be able to exist all on it’s own with only the data from the URL. When tapping into some item to show it bigger, for existence, we don’t want to depend on that having been fetched from the list. Each screen can stand on it’s own though obviously we can use the data if it’s already in the store. Being URL-driven helps us there.

We found it useful to even use the NavigationBar even when we didn’t “need” it. That is, the sample app’s signup/login experience doesn’t show a navigation bar. We still depend on all the URL routing and rendering, though. So it just get’s hidden and we still get all that from offscreen.

Other App Stuff

Here are some of the other things in the sample app that we’re not planning on talking much more about. Let us know if something could benefit from a more in-depth look.

Flux

The Components use Actions. Actions tend to use the API Services and dispatch an event. The Stores are listening to the events. The Components add and remove listeners to the Stores.

Environment

There is a model called Environment that gets bootstrapped from Objective-C. It knows things that are different per environment like what API server to talk to.

Data storage

Info is currently stored as json to the local file system.

Shared CSS

It uses the cssVar pattern from the sample Facebook apps.

API

It uses superagent to do HTTP requests and sets headers and other things like that.

Components

Some shared components that might be helpful

  • SegmentedControl: Non-iOS specific version of that control
  • SimpleList: make a list out of the props set
  • Button: Helper to make them all similiar

Mixins

We are currently sharing code through mixins. Some of them might be generally useful.

  • KeyboardListener: helps know the amount of space the keyboard is taking up
  • DispatcherListener: subscribes and ubsubscribes from the Dispatcher for a component
  • NavigationListener: react to navigation changes in a component

Server

There is a server you can run locally using cd server && npm start that supports the app. It seeds some data and will save (in memory) anything the app sends it.

Copyright © 2017 Brian Leonard