React Native Navigation solutions in 2018

Adrien Thiery
Oct 26, 2018 · 13 min read

W hen starting your React Native application today, considering the diversity of the package ecosystem, it is understandable to be confused about which library to use, especially for navigation.

Lots of solutions are out there:

More than that are available, but I selected the ones that seemed to be the most popular (and the ones I heard of recently) for this comparison.

Full disclosure: the only libraries we have used in production at OSEDEA are react-navigation, react-native-navigation and react-native-router-flux.

TL;DR

You can find the code of the following examples on Github: https://github.com/osedea/react-native-navigation-examples

What this article consists of

In this article, we are going to implement a simple navigation scheme (Stack navigation) with all of these libraries to compare the way they work and the advantages of one over another.

All these example are created with the latest React Native version at the time of writing (0.57.2) except for ReactNativeNavigationV2, because wix’s v2 of react-native-navigation only supports 0.56.* as the highest version of React Native at this time.

But first, a little explanation of the differences between the two big families of navigation libraries.

Native navigation vs JS navigation

In a Native mobile application, each screen has a native container (UIViewController on iOS, Activity on Android).

This allows the iOS and Android systems to do optimizations to save some CPU or memory.

React Native’s bridge system allows us to use Native components, for example navigation components (NavBar, Back button, etc.).

The only issue is that when using them like that, they are out of the React Root View scope, which makes them harder to inspect (the React Native inspector JS and is actually injected into the Root View of React Native).

Let’s use images to visualize this:

Native navigation creates a Native container for each view (the red border rectangle), which allows the system to optimize and use less processing power and battery. That however creates a limited React scope (the green border rectangle).

In comparison, JS navigation is a bit less optimized:

We see that all the views are rendered in the same native container, on top of one another, which makes the processing of any operation a little bit slower.

To look at the different navigation libraries, here is a small legend to help you:

🍏: Library works on iOS
🤖: Library works on Android
🌎: Library is purely JS
📱: Library is using Native components

NavigatorIOS 🍏📱

NavigatorIOS is the legacy navigation component that has come with React Native since the beginning. As the story goes, as Facebook inserted React Native into their native mobile apps, they did not have a need for navigation components/utilities.

For the sake of history, let’s look at how it works.

As the documentation states:

NavigatorIOS is a wrapper around UINavigationController, enabling you to implement a navigation stack. It works exactly the same as it would on a native app using UINavigationController, providing the same animations and behaviour from UIKit.

Defining your initial route needs to be done in your root component, and passed as prop to NavigatorIOS:

// App.jsimport { NavigatorIOS } from "react-native";import Home from "./scenes/Home";type Props = {};export default class App extends Component<Props> {
render() {
return (
<NavigatorIOS
initialRoute={{
component: Home,
title: "Home"
}}
/>
);
}
}

But then, each component needs to import the components it wants to navigate to, which can be very complex in some cases:

// scenes/Home.jsimport PushedView from "./PushedView";type Props = {
navigator: Object
};
export default class Home extends Component<Props> {
goToPushedView = () => {
this.props.navigator.push({
title: "PushedView",
component: PushedView
});
};
render() {
return (
<View>
<Button
onPress={this.goToPushedView}
title={"Push something"}
/>
</View>
);
}
}

Keep pushing!

// scenes/PushedView.jstype Props = {
navigator: object
};
export default class PushedView extends Component<Props> {
goToPushedView = () => {
this.props.navigator.push({
title: "PushedView",
component: PushedView
});
};
render() {
return (
<View>
<Text>"I'm pushed!"</Text>
<Button
onPress={this.goToPushedView}
title={"Push more"}
/>
</View>
);
}
}

We see it’s native, as it has all blank views for screens that are under the 1st one in the stack:

Native hierarchy with 5 screens pushed with NavigatorIOS

As expected when reading its name, the Android counterpart is not rendering anything. Duh.

This shows us that if we want to build a full React Native application, we need an external library.

EDIT (10/11/2018): As mentioned in the comments by Lorenzo Sciandra, NavigatorIOS will be fully removed from React Native in version 0.58 (see the React Native Changelog)

react-navigation 🤖🍏🌎

How to see if a library is Native or JS? Check the github code composition!

In addition of being the one mentioned in the official documentation, this is the most used navigation library of all (13556 ⭐️ on github at the time of writing).

Installation is as simple as:

npm install react-navigation
// OR
yarn add react-navigation

and you’re all set!

Defining your routes goes through a create<Choose your pattern>Navigator function (createStackNavigator , createBottomTabsNavigator , etc.)

// App.jsimport { createStackNavigator } from "react-navigation";import Home from "./scenes/Home";
import PushedView from "./scenes/PushedView";
export default createStackNavigator({
Home: {
screen: Home,
navigationOptions: {
title: "Home"
}
},
PushedView: {
screen: PushedView
}
});

Navigating is actually as simple as it sounds! Using the navigation prop injected by react-navigation into each Screen component.

// scenes/Home.jstype Props = {
navigation: Object
};
export default class Home extends Component<Props> {
goToPushedView = () => {
this.props.navigation.navigate("PushedView");
};
render() {
return (
<View>
<Button
onPress={this.goToPushedView}
title={"Push something"}
/>
</View>
);
}
}

Keep pushing!

// scenes/PushedView.jstype Props = {
navigation: Object
};
export default class PushedView extends Component<Props> {
goToPushedView = () => {
this.props.navigation.push("PushedView");
};
render() {
return (
<View>
<Text>"I'm pushed!"</Text>
<Button
onPress={this.goToPushedView}
title={"Push more"}
/>
</View>
);
}
}

You’ll notice that we switch from navigate to push in the PushedView. This adds a small distinction: when navigating, if you try to visit the same screen twice, it won’t work. navigate makes our screen behave like a singleton in the stack. However, when using push , we can add infinitely push new screens onto our stack.

Although we see in XCode that the other views in the stack are not optimized

EDIT (10/11/2018): As mentioned by Brent Vatne in the comments, this can be optimized using react-native-screens and will be the default in react-navigation in the future. (See this tweet from Janic Duplessis for the XCode visualization)

react-native-navigation 🤖🍏📱

React Native Navigation provides 100% native platform navigation on both iOS and Android for React Native apps.

v1 Or v2 ?

Right now, the choice is a hard one, as v1 is not maintained anymore but is working albeit with some quirks, and v2 is still in alpha.

EDIT (19/11/2018): v2 is now out of alpha!

The hardest part is actually at the beginning: the setup.

Setup (v1)

Since it is native, we need to setup some native stuff. I won’t go too much into detail because, thanks to ignite - a great tool allowing you to speed up your React Native development - and the ignite-native-navigation plugin, the pain is gone.

ignite attach # to attach to your React Native project
ignite add native-navigation

You’re good to go!

Registering your routes (v1)

In this case, no more AppRegistry.registerComponent() in our index.js, react-native-navigation is handling the startup of the app a different way!

// index.jsimport { Navigation } from "react-native-navigation";import Home from "./scenes/Home";
import PushedView from "./scenes/PushedView";
Navigation.registerComponent(
"ReactNativeNavigationV1.Home",
() => Home
);
Navigation.registerComponent(
"ReactNativeNavigationV1.PushedView",
() => PushedView
);
Navigation.startSingleScreenApp({
screen: {
screen: "ReactNativeNavigationV1.Home",
title: "Home"
}
});

The rest is very similar to react-navigation :

// scenes/Home.jstype Props = {
navigator: Object
};
export default class Home extends Component<Props> {
goToPushedView = () => {
this.props.navigator.push({
screen: "ReactNativeNavigationV1.PushedView",
title: "PushedView"
});
};
render() {
return (
<View>
<Button
onPress={this.goToPushedView}
title={"Push something"}
/>
</View>
);
}
}

And we can continue pushing:

// scenes/PushedView.jstype Props = {
navigator: Object
};
export default class PushedView extends Component<Props> {
goToPushedView = () => {
this.props.navigator.push({
screen: "ReactNativeNavigationV1.PushedView",
title: "PushedView"
});
};
render() {
return (
<View>
<Text>"I'm pushed!"</Text>
<Button
onPress={this.goToPushedView}
title={"Push something"}
/>
</View>
);
}
}
Layout structure on iOS after 5 pushes

Setup (v2)

For v2, sadly no magic tool is there to help us. I tried using ignite-react-native-navigation to cheat a little bit, to no avail. I guess we have to follow the guide for this one.

After following the guide very carefully, you should have your app starting nicely.

// index.jsimport { Navigation } from "react-native-navigation";import Home from "./scenes/Home";
import PushedView from "./scenes/PushedView";
Navigation.registerComponent(
`ReactNativeNavigationV2.Home`,
() => Home
);
Navigation.registerComponent(
`ReactNativeNavigationV2.PushedView`,
() => PushedView
);
Navigation.events().registerAppLaunchedListener(() => {
Navigation.setRoot({
root: {
stack: {
children: [{
component: {
name: 'ReactNativeNavigationV2.Home',
options: {
topBar: {
title: {
text: 'Home'
}
}
}
}
}]
}
}
});
});

We can see already that the code is way more verbose, pushing a single screen with a title requires quite a few lines and nested objects, but that’s all for the sake of readability and customization.

// screens/Home.js (PushedView.js is highly similar)import { Navigation } from "react-native-navigation";type Props = {};export default class Home extends Component<Props> {
goToPushedView = () => {
Navigation.push(this.props.componentId, {
component: {
name: 'ReactNativeNavigationV2.PushedView',
options: {
topBar: {
title: {
text: 'PushedView'
}
}
}
}
});
};
render() {
return (
<View>
<Button
onPress={this.goToPushedView}
title={"Push something"}
/>
</View>
);
}
}
Same optimizations (only one header, only one view fully rendered)

react-native-router-flux 🤖🍏🌎

react-native-router-flux is a different API over react-navigation.

It offers another way to define your routes, in JSX directly, like any other React component.

// App.jsimport { Router, Stack, Scene } from "react-native-router-flux";import Home from "./scenes/Home";
import PushedView from "./scenes/PushedView";
const App = () => (
<Router>
<Stack key="root">
<Scene
key="home"
component={Home}
title="Home"
/>
<Scene
key="pushedView"
component={PushedView}
/>
</Stack>
</Router>
);
export default App;
JS it is, for sure!

react-native-easy-router 🤖🍏🌎

yarn add react-native-easy-router

react-native-easy-router is as easy it sounds.

// App.jsimport React from 'react';
import Router from 'react-native-easy-router';
import { Text, View } from 'react-native';
import Home from "./scenes/Home";
import PushedView from "./scenes/PushedView";
const routes = { Home, PushedView };const App = () => <Router routes={routes} initialRoute="Home" />;export default App;

But it also does not bring a lot of features, which is the whole point.

Navigating is a mix of react-navigation and react-native-router-flux :

// scenes/Home.jstype Props = {
router: Object
};
export default class Home extends Component<Props> {
goToPushedView = () => {
this.props.router.push.PushedView();
};
render() {
return (
<View>
<Button
onPress={this.goToPushedView}
title={"Push something"}
/>
</View>
);
}
}

And the result is, well, simple. But works perfectly out of the box.

We see that here as well, it’s full JS, so every screen is rendered at the same time

react-native-swipe-navigation 🤖🍏🌎

yarn add react-native-swipe-navigation

This last library is pretty simple to use, and offers swiping as a new pattern to easily reproduce an app like Snapchat.

It allows you to easily define a screen for each direction you can swipe, and provides a smooth animation to transition between screens.

// App.jsimport SwipeNavigator from 'react-native-swipe-navigation';import Home from "./scenes/Home";
import PushedView from "./scenes/PushedView";
const Navigator = SwipeNavigator({
Home: {
screen: Home,
right: 'PushedView',
},
PushedView: {
screen: PushedView,
type: 'push',
right: 'PushedView',
},
});
export default Navigator;

The allowed gestures/moves are defined globally at the initialization of the application. Then, in each component, callbacks are used to allow/deny the swipe action’s navigation.

// scenes/Home.jstype Props = {
nav: Object
};
export default class Home extends Component<Props> {
componentDidMount() {
const { nav } = this.props;
nav.onNavigateShouldAllow(() => {
return true;
});
nav.onNavigateRightShouldAllow(() => {
return true;
});
}
componentWillUnmount() {
this.props.nav.cleanUp()
}
goToPushedView = () => this.props.nav.navigate('PushedView'); render() {
return (
<View>
<Button
onPress={this.goToPushedView}
title={"Push something"}
/>
</View>
);
}
}
As JS as it gets!

react-router-native 🤖🍏🌎

Such a diverse code base composition!

react-router-native is kind of a surprise to me because I didn’t here a lot of things about it.

Being the native bindings of react-router, what it brings to the table is interesting, especially for teams who are coming from the web and want to start using React Native, as it gives you the same interface.

yarn add react-router-native

As for react-router on the web, routes are defined as React components, in the view hierarchy.

// App.jsimport { NativeRouter, Route, Link } from "react-router-native";import Home from "./scenes/Home";
import PushedView from "./scenes/PushedView";
type Props = {};export default class App extends Component<Props> {
render() {
return (
<NativeRouter>
<View>
<Route
exact
path="/"
component={Home}
/>
<Route
path={"/pushed"}
component={PushedView}
/>
</View>
</NativeRouter>
);
}
}

To navigate from screen to screen, we just need to use the Link component, which actually wraps a TouchableHighlight component, so you can style it as you would a TouchableHighlight.

// scenes/Home.jsimport { Link } from "react-router-native";type Props = {};export default class Home extends Component<Props> {
render() {
return (
<View style={styles.container}>
<Link to="/pushed">
<Text>{'Push something'}</Text>
</Link>
</View>
);
}
}
Funny enough, even if it is JS, views in the stack seem to be optimized!

Conclusion

It all comes down to what you need.

If you just want screen navigation, in JS, with a small footprint on your bundle size, react-native-easy-router might be the best fit, but you’ll have to handle the header yourself.

If you want the best performance out there, even with a bit more complexity, go for react-native-navigation.

If you don’t really know what your app is going to become, but you want flexibility and the ability to customize everything, I would recommend going with react-navigation.

If you just need to create a simple iOS app and do not want to add another library, just use NavigatorIOS.

If you are porting your React web app, react-router-native might be the best solution for you.

Finally, if you want to clone Snapchat, go with react-native-swipe-navigation.

You can find all of this code at: https://github.com/osedea/react-native-navigation-examples with working examples.

Feel free to contact me for any question/inquiry on Twitter or on OSEDEA’s website.

Also, we’re hiring! 👩‍🎓👨‍🎓

Please contact us if you are looking for opportunities in Montreal, Québec or drop for a cup of coffee if you’re in town!

OSEDEA

Montreal based tech agency taking work-life harmony to a higher level.

Thanks to Nathaniel Ram

Adrien Thiery

Written by

Technical Human @osedea. React Native & React by day, Python & VueJS by night.

OSEDEA

OSEDEA

Montreal based tech agency taking work-life harmony to a higher level.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade