Visit The School

Making React Native Navigator Work Like NavigatorIOS

Alexander Paterson
|
Posted 10 months ago
|
5 minutes
Making React Native Navigator Work Like NavigatorIOS
Use Navigator to keep your apps cross-platform

Here, I'll show you how to set up the Navigator component to work as simply as NavigatorIOS. I think the React Native documentation is lacking with regards to this.

NavigatorIOS

The NavigatorIOS component works out of the box. Simply stick it in your JSX:

// app/components/App.js

//...

var App = React.createClass({
  render() {
    return (
      <NavigatorIOS
        style={{flex: 1}}
        initialRoute={{
          title: 'Log In',
          component: Login
        }}/>
    );
  }
});

//...

And you're good to start pushing and popping routes:

// app/components/Login.js

//...

var Login = React.createClass({
  pushToMain: function() {
    this.props.navigator.push({
      title: "Main",
      component: Main,
      passProps: {email: this.state.email}
    });
  },

  //...

passProps is passed down, and you even get a navigation bar for free. (The navigation bar can be hidden by giving the route object a property of navigationBarHidden set to true.)

Navigator

If you've looked at the React Native documentation for the Navigator component, you'll find things aren't so simple. Navigator is given a renderScene prop which returns JSX to be rendered, in response to having a route pushed onto its stack. This is from the React Native documentation:

render() {
  const routes = [
    {title: 'First Scene', index: 0},
    {title: 'Second Scene', index: 1},
  ];
  return (
    <Navigator
      initialRoute={routes[0]}
      initialRouteStack={routes}
      renderScene={(route, navigator) =>
        <TouchableHighlight onPress={() => {
          if (route.index === 0) {
            navigator.push(routes[1]);
          } else {
            navigator.pop();
          }
        }}>
        <Text>Hello {route.title}!</Text>
        </TouchableHighlight>
      }
      style={{padding: 100}}
    />
  );
}

This is not pretty. However, by attaching components to our route objects, we can set this up to behave exactly as NavigatorIOS does:

// app/components/App.js

// ...

var routes = [
  {
    component: Login,
    title: 'Log In'
  }
];

var App = React.createClass({

  //...

  render() {
    return (
      <Navigator
        initialRoute={routes[0]}
        initialRouteStack={routes}
        renderScene={(route, navigator) => {
          return (
            <route.component navigator={navigator} {...route.passProps}/>
          )
        }}
        style={{flex: 1}}/>
    );
  }

  //...

Now this will work in place of NavigatorIOS. One thing we need to address, however, is the lack of a navigation bar. You have to implement this on your own; I suggest creating a reusable Navbar component, like the following:

// app/components/shared/Navbar.js

import React from 'react';
import Icon from 'react-native-vector-icons/Octicons'
import {
  StyleSheet,
  Text,
  View,
  TouchableOpacity
} from 'react-native';


var Navbar = React.createClass({
  onLeftButtonPress() {
    this.props.onLeftButtonPress();
  },
  onRightButtonPress() {
    this.props.onRightButtonPress();
  },
  render() {
    var renderLeftButton = () => {
      if (!this.props.leftButtonHidden) {
        return (
          <TouchableOpacity onPress={this.onLeftButtonPress}>
            <Icon name={this.props.leftIconName} size={15} color="white" style={{padding:10}}/>
          </TouchableOpacity>
        )
      } else {
        return (
          <TouchableOpacity onPress={this.onLeftButtonPress}>
            <Icon name="search" size={15} color="rgba(0,0,0,0)" style={{padding:10}}/>
          </TouchableOpacity>
        )
      }
    }
    var renderRightButton = () => {
      if (!this.props.rightButtonHidden) {
        return (
          <TouchableOpacity onPress={this.onRightButtonPress}>
            <Icon name={this.props.rightIconName} size={15} color="white" style={{padding:10}}/>
          </TouchableOpacity>
        )
      } else {
        return (
          <TouchableOpacity onPress={this.onRightButtonPress}>
            <Icon name="search" size={15} color="rgba(0,0,0,0)" style={{padding:10}}/>
          </TouchableOpacity>
        )
      }
    }
    return (
      <View style={styles.topBar}>
        {renderLeftButton()}
        <Text style={styles.title}>
          {this.props.title.toUpperCase()}
        </Text>
        {renderRightButton()}
      </View>
    );
  }
});

const styles = StyleSheet.create({
  topBar: {
    padding: 8,
    flexDirection: 'row',
    justifyContent: 'space-between',
    paddingTop: 24,
    backgroundColor: '#2dbecd',
    alignItems: 'center',
    height: 90,
  },
  title: {
    color: 'white',
    fontFamily: 'Helvetica',
    fontSize: 18,
  }
});

module.exports = Navbar;

Which can be used as follows:

// app/components/Main.js

//...

  render() {
    return (
      <View style={styles.container}>
        <Navbar
          onLeftButtonPress={() => this.props.navigator.pop())}
          title="Main"
          rightButtonHidden={true}
          leftIconName="chevron-left"/>

//...

One final caveat is that you'll want to give your navigation-level components (Login, Main, etc) background colours, or the previous route will be visible beneath them.



ALEXANDER
PATERSON