Skip to main content
Back to blog

For developers

React Native navigation solutions in 2018

Adrien Thiery
Oct 26 ∙ 15 mins
React Native navigation solutions in 2018

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:

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 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:

blogpost rnn image 1

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:

JS navigation has only one native container and adds layers of views on top of one another, which is heavier for the system to handle
JS navigation has only one native container and adds layers of views on top of one another, which is heavier for the system to handle

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:

NavigatorIOSis 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.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>
        );
    }
}

This looks exactly like the native iOS animation, and that’s normal, because it is! Although that bug occurring straight out of the box shows how well maintained NavigatorIOS is 😅

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
Native hierarchy with 5 screens pushed with NavigatorIOS

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

blogpost rnn image 5

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!
How to see if a library is Native or JS? Check the github code composition!

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.

Using react-navigation to push screens, we see that the library handles a lot of the UI by default. Including Header animations

As expected, the same code works perfectly on Android, but using Android design patterns
As expected, the same code works perfectly on Android, but using Android design patterns

Although we see in XCode that the other views in the stack are not optimized
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 🤖🍏📱

blogpost rnn image 10

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>
        );
    }
}

react-native-navigation does deliver simple native navigation for React Native on both iOS and Android
react-native-navigation does deliver simple native navigation for React Native on both iOS and Android

Layout structure on iOS after 5 pushes
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.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>
        );
    }
}

Exactly the same as v1, except the code.
Exactly the same as v1, except the code.

Same optimizations (only one header, only one view fully rendered)
Same optimizations (only one header, only one view fully rendered)

react-native-router-flux 🤖🍏🌎

blogpost rnn image 15

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

It offers another way to define your routes, in JSXdirectly, 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;

We see here that the implementation is a tiny bit different, noticeably the animation in the Header that does not fit the Native one
We see here that the implementation is a tiny bit different, noticeably the animation in the Header that does not fit the Native one

JS it is, for sure!
JS it is, for sure!

react-native-easy-router 🤖🍏🌎

blogpost rnn image 17

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.

screen shot 2020 06 16 at 1 37 36 pm

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

react-native-swipe-navigation 🤖🍏🌎

blogpost rnn image 19

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>
        );
    }
}

Sorry for the artifact on the gif, I couldn’t make it work better

As JS as it gets!
As JS as it gets!

react-router-native 🤖🍏🌎

Such a diverse code base composition!
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.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 Linkcomponent, 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>
        );
    }
}

No styling comes out of the box, no header and no animation, but it works on both platforms!

Funny enough, even if it is JS, views in the stack seem to be optimized!
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 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!