Update, November 14 2016:

It's been brought to my attention that a fix has been implemented in React Native v0.38 from this commit that fixes the underlying issue that causes NavigatorIOS components to not be usable when nested within each other.

Moving forward with the official stable release of v0.38, you should be able to nest NavigatorIOS within each other, therefore deprecating part of this post :)

TabBars are intimidating.

You know how you open your favorite application and it has that tab bar at the bottom with different options?

And then as you go deep into one of those tabs, that bar still stays exactly where it is?

And when you finally tap on the same icon again after being a million layers deep it pulls you right back to the beginning?

Ever wonder how to make it happen? I have, so I decided to find out.

It's not as hard as it looks.

We'll have to lay out some building blocks in order to accomplish this. Let's take a look at how we'll structure our application, though:

What we're going to do is structure index.ios.js to render a Navigator component whose initialRoute will contain our tabs:

var PHReactNative = React.createClass({

  renderScene: function(route, navigator) {
    var Component = route.component;
    return (
      <View style={styles.container}>
        <Component
          route={route}
          navigator={navigator}
          topNavigator={navigator} />
      </View>
      )
  },

  render: function() {
    return (
      <Navigator
        sceneStyle={styles.container}
        ref={(navigator) => { this.navigator = navigator; }}
        renderScene={this.renderScene}
        tintColor='#D6573D'
        barTintColor='#FFFFFD'
        titleTextColor='#D6573D'
        navigationBarHidden={true}
        initialRoute={{
          title: 'Product Kitty',
          component: Main,
        }} />
    );
  }
});

From here, we'll structure Main to contain our TabBarIOS component as such. Note that I'm using Icon.TabBarItem, which is a custom TabBarItem plugin from this really sweet package.

  render: function() {
    return (
      <TabBarIOS>
        <Icon.TabBarItem
          title='Home'
          selected={this.state.selectedTab === 'products'}
          iconName={'home'}
          iconSize={20}
          onPress={() => {
            this.setState({
              selectedTab: 'products'
            });
          }}>
          {this.renderProductView()}
        </Icon.TabBarItem>
        <Icon.TabBarItem
          title="Collections"
          selected={this.state.selectedTab === 'collections'}
          iconName={'list'}
          iconSize={20}
          onPress={() => {
            this.setState({
              selectedTab: 'collections'
            });
          }}>
          {this.renderCollectionView()}
        </Icon.TabBarItem>
      </TabBarIOS>
      )
  },

Then, renderProductView and renderCollectionView will each render their own personal NavigatorIOS components.

It's important to note here that if the parent Navigator were a NavigatorIOS component instead, our application would suffer a crash as outlined in this Github issue.

  renderProductView: function() {
    return (
      <NavigatorIOS
        style={styles.container}
        tintColor='#D6573D'
        barTintColor='#FFFFFD'
        titleTextColor='#D6573D'
        initialRoute={{
          title: 'Product Kitty',
          component: Products
        }} />
        )
  },

  renderCollectionView: function() {
    return (
      <NavigatorIOS
        style={styles.container}
        tintColor='#D6573D'
        barTintColor='#FFFFFD'
        titleTextColor='#D6573D'
        initialRoute={{
          title: 'Collections',
          component: Collections,
          passProps: {
            accessToken: this.state.accessToken
          }
        }} />
        )
  }

Great, that's nearly it. But we still don't have functionality to go to the top of our Navigator stack if we go too deep in, and we'd hate to rely on that back button so many times.

First, we'll need to assign each NavigatorIOS component its own refs:

ref='productRef' and ref='collectionRef' should do it, updating our code block above to look like:

  renderProductView: function() {
    return (
      <NavigatorIOS
        style={styles.container}
        tintColor='#D6573D'
        barTintColor='#FFFFFD'
        titleTextColor='#D6573D'
        ref='productRef'
        initialRoute={{
          title: 'Product Kitty',
          component: Products
        }} />
        )
  },

  renderCollectionView: function() {
    return (
      <NavigatorIOS
        style={styles.container}
        tintColor='#D6573D'
        barTintColor='#FFFFFD'
        titleTextColor='#D6573D'
        ref='collectionRef'
        initialRoute={{
          title: 'Collections',
          component: Collections,
          passProps: {
            accessToken: this.state.accessToken
          }
        }} />
        )
  }

Now let's add a conditional statement to our onPress method for these Tab Bar Items. We're going to check to see if, when tapped, a Tab Bar Item is also the currently selected tab. If it isn't, we'll carry on as usual. Otherwise, we're going to call pushToPop() on it, and we can make sure we grab the right NavigatorIOS by using this.refs:

  onPress={() => {
    if (this.state.selectedTab !== 'products') {
      this.setState({
        selectedTab: 'products'
      });
    } else if (this.state.selectedTab === 'products') {
      this.refs.productRef.popToTop();
    }

Okay, time to wrap it up. Here's how our entire Main component looks, after all these changes:

var Main = React.createClass({  
  getInitialState: function() {
    return {
      accessToken: false,
      selectedTab: 'products'
    }
  },

  componentWillMount: function() {
    if (!this.state.accessToken){
    api.getToken()
      .then((responseData) => {
        this.setState({
          accessToken: responseData.access_token,
        });
      })
      .done();
    }
  },

  render: function() {
    return (
      <TabBarIOS>
        <Icon.TabBarItem
          title='Home'
          selected={this.state.selectedTab === 'products'}
          iconName={'home'}
          iconSize={20}
          onPress={() => {
            if (this.state.selectedTab !== 'products') {
              this.setState({
                selectedTab: 'products'
              });
            } else if (this.state.selectedTab === 'products') {
              this.refs.productRef.popToTop();
            }
          }}>
          {this.renderProductView()}
        </Icon.TabBarItem>
        <Icon.TabBarItem
          title="Collections"
          selected={this.state.selectedTab === 'collections'}
          iconName={'list'}
          iconSize={20}
          onPress={() => {
            if (this.state.selectedTab !== 'collections') {
              this.setState({
                selectedTab: 'collections'
              });
            } else if (this.state.selectedTab === 'collections') {
              this.refs.collectionRef.popToTop();
            }
          }}>
          {this.renderCollectionView()}
        </Icon.TabBarItem>
      </TabBarIOS>
      )
  },

  renderProductView: function() {
    return (
      <NavigatorIOS
        style={styles.container}
        tintColor='#D6573D'
        barTintColor='#FFFFFD'
        titleTextColor='#D6573D'
        ref='productRef'
        initialRoute={{
          title: 'Product Kitty',
          component: Products
        }} />
        )
  },

  renderCollectionView: function() {
    return (
      <NavigatorIOS
        style={styles.container}
        tintColor='#D6573D'
        barTintColor='#FFFFFD'
        titleTextColor='#D6573D'
        ref='collectionRef'
        initialRoute={{
          title: 'Collections',
          component: Collections,
          passProps: {
            accessToken: this.state.accessToken
          }
        }} />
        )
  }

})

Now, let's take a look at our amazing functionality in action: