When 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:
- NavigatorIOS
- react-navigation
- Wix’s react-native-navigation (v1 & v2)
- AirBNB’s native-navigation (just kidding, it has never really been alive, and with AirBNB sunsetting React Native, I doubt it will ever be)
- react-native-router-flux
- react-native-easy-router
- react-native-swipe-navigation
- react-router-native
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
- If you are new to React Native but want to create a production app, choose react-navigation
- If you’re knowledgeable about React Native and know your app will need high performance (extensive rotation interactions, or going deep into your stacks, for example), go with react-native-navigation v2
- If you are porting your web application using react-router, go for react-router-native
- If you just want to play with something new, go with react-native-swipe-navigation You can find the code of the following examples on Github.
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 examples 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 aroundUINavigationController
, enabling you to implement a navigation stack. It works exactly the same as it would on a native app usingUINavigationController
, 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.js
import { 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.js
import 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.js
type 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:
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 🤖🍏🌎
In addition to 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.js
import { 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.js
type 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.js
type 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.
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 set up 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.js
import { 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.js
type 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.js
type 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>
);
}
}
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.js
import { 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>
);
}
}
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.js
import { 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;
react-native-easy-router 🤖🍏🌎
yarn add react-native-easy-router
react-native-easy-router is as easy it sounds.
// App.js
import 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.js
type 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.
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.js
import 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.js
type 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>
);
}
}
react-router-native 🤖🍏🌎
react-router-native is kind of a surprise to me because I didn’t hear 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.js
import { 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.js
import { 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>
);
}
}
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: Github - Osedea React Native examples
—
Feel free to contact me for any questions/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!