Easily Work on Deeply Nested Screens in React Native

Author

Spencer Carli

Developer, cat dad, and devout pizza lover. Teaching at React Native School and building apps with Handlebar Labs.

Last Updated: March 13, 2019

Let's say you've been working on an app for a few weeks (or longer) and you've got a handful of screens. In one stack you need to navigate through three other screens before you get to the one you're currently working on.

This is tedious and time consuming. So what can you do about it?

Option 1: Hot Reloading

The easiest option may come directly from React Native in the form of hot reloading. Hot reloads basically do an update in place vs. restarting the JavaScript in your app.

You can enable hot reloading from the debug menu.

In my experience hot reloading can be a bit iffy. If you're seeing the same, or you're just looking for an alternative solution, we can try:

Option 2: Hacking Your Navigation

First, let's set the stage with a basic app. I'm keeping everything in one file for simplicity of the tutorial. I wouldn't actually build it this way - that's not the point of this tutorial.

App.js

import React from 'react';
import { Text, View, Button } from 'react-native';
import {
  createAppContainer,
  createStackNavigator,
  createBottomTabNavigator,
  createSwitchNavigator,
} from 'react-navigation';

const Screen = ({ title, navigation, nextScreen, nextScreenData }) => (
  <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
    <Text style={{ fontSize: 20 }}>{title}</Text>
    {nextScreen && (
      <Button
        title={`Go to ${nextScreen}`}
        onPress={() => navigation.navigate(nextScreen, nextScreenData)}
      />
    )}
    {navigation.state && navigation.state.params && (
      <Text>{JSON.stringify(navigation.state.params, null, 2)}</Text>
    )}
  </View>
);

const App = createStackNavigator(
  {
    Tabs: createBottomTabNavigator({
      Home: {
        screen: createStackNavigator({
          Stations: {
            screen: (props) => (
              <Screen
                title="Stations"
                nextScreen="Vehicles"
                nextScreenData={{ stationId: 14 }}
                {...props}
              />
            ),
            navigationOptions: {
              headerTitle: 'Stations',
            },
          },
          Vehicles: {
            screen: (props) => (
              <Screen
                title="Vehicles"
                nextScreen="Locations"
                nextScreenData={{ vehicleId: 1401 }}
                {...props}
              />
            ),
            navigationOptions: {
              headerTitle: 'Stations',
            },
          },
          Locations: {
            screen: (props) => (
              <Screen
                title="Locations"
                nextScreen="Tools"
                nextScreenData={{ locationId: 123 }}
                {...props}
              />
            ),
            navigationOptions: {
              headerTitle: 'Stations',
            },
          },
          Tools: {
            screen: (props) => <Screen title="Tools" {...props} />,
            navigationOptions: {
              headerTitle: 'Stations',
            },
          },
        }),
      },
      Profile: {
        screen: (props) => (
          <Screen title="Profile" nextScreen="Auth" {...props} />
        ),
      },
    }),
  },
  {
    mode: 'modal',
    headerMode: 'none',
  }
);

const Auth = createStackNavigator({
  SignIn: {
    screen: (props) => (
      <Screen title="Sign In" nextScreen="SignUp" {...props} />
    ),
    navigationOptions: {
      headerTitle: 'Sign In',
    },
  },
  SignUp: {
    screen: (props) => <Screen title="Sign Up" nextScreen="App" {...props} />,
    navigationOptions: {
      headerTitle: 'Sign Up',
    },
  },
});

const RootNavigation = createSwitchNavigator({
  Auth: {
    screen: Auth,
  },
  App: {
    screen: App,
  },
});

export default createAppContainer(RootNavigation);
Quick App Demo

What we've got here is an app with a sign in screen, sign up screen, profile screen, and then a stack of screens. Stations > Vehicles > Locations > Tools.

We want to work on the tools screen. But I don't want to be clicking a bunch of times to get there.

App Entry Point

We're going to be following a pattern I outlined in a previous lesson, Setting up an Authentication Flow in React Native where we have an entry point screen where we figure out where the user should go when they open the app.

Normally you would check authentication status on this screen but in this lesson we'll look at how to leverage it to do our navigation for us, automatically.

First, let's add the screen to our RootNavigation.

App.js

// ...

class Entry extends React.Component {
  render() {
    return (
      <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
        <Text>Loading...</Text>
      </View>
    );
  }
}

const RootNavigation = createSwitchNavigator({
  Entry: {
    screen: Entry,
  },
  Auth: {
    screen: Auth,
  },
  App: {
    screen: App,
  },
});

export default createAppContainer(RootNavigation);

We're now stuck with a loading screen...

Loading Screen

Automatic Navigation

We'll want to do all of our work in componentDidMount in the Entry screen.

First, we'll create a new variable, INITIAL_SCREEN, which we'll use to do our automatic navigation. If INITIAL_SCREEN has a length, navigate!

App.js

// ...

const INITIAL_SCREEN = 'Tools';

class Entry extends React.Component {
  componentDidMount() {
    // Do your normal auth checking and navigation here.

    this.navigateToDevelopmentScreen();
  }

  navigateToDevelopmentScreen = () => {
    if (INITIAL_SCREEN && INITIAL_SCREEN.length) {
      this.props.navigation.navigate(INITIAL_SCREEN);
    }
  };

  render() {
    return (
      <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
        <Text>Loading...</Text>
      </View>
    );
  }
}

// ...
Automatically navigate to the tools screen.

Handling Required Data

Now, this presents a problem. Most of our screens don't live in isolation, especially nested screens. They likely depend on some data from a previous screen.

In my example's case I need to know the locationId to get the required tools. With that, I need to pass those params down so we'll need a minor refactor.

App.js

// ...

const INITIAL_SCREEN = {
  screen: 'Tools',
  params: {
    locationId: 123,
  },
};

class Entry extends React.Component {
  componentDidMount() {
    // Do your normal auth checking and navigation here.

    this.navigateToDevelopmentScreen();
  }

  navigateToDevelopmentScreen = () => {
    if (INITIAL_SCREEN.screen && INITIAL_SCREEN.screen.length) {
      this.props.navigation.navigate(
        INITIAL_SCREEN.screen,
        INITIAL_SCREEN.params
      );
    }
  };

  render() {
    return (
      <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
        <Text>Loading...</Text>
      </View>
    );
  }
}

// ...

INITIAL_SCREEN becomes an object which store both a screen and params. Those params are passed as a second argument to our navigate function which gives us the required data for that screen.

Demo of automatic navigation with data

You can go ahead and extend this to have everything necessary for a variety of screens.

App.js

// ...

const demoRoutesAndParams = {
  Vehicles: {
    screen: 'Vehicles',
    params: {
      stationId: 14,
    },
  },
  Tools: {
    screen: 'Tools',
    params: {
      locationId: 123,
    },
  },
};

const INITIAL_SCREEN = demoRoutesAndParams.Tools;

// ...

How should you get the code for the params? The easiest way would to be just add a console.log(JSON.stringify(this.props)) to the render method of the target screen (Tools in our above example), copy that, and add it to the demoRoutesAndParams variable.

Protecting Production

Now a caveat to this. As it stands, it's dangerous. If you forget to set INITIAL_SCREEN.screen to an empty string and build for production you're going to have some confused users on your hands. To avoid that check that you're in development before doing the automatic navigation.

React Native exposes a __DEV__ that will tell you if you're in development.

App.js

// ...
class Entry extends React.Component {
  componentDidMount() {
    // Do your normal auth checking and navigation here.

    this.navigateToDevelopmentScreen();
  }

  navigateToDevelopmentScreen = () => {
    if (__DEV__ && INITIAL_SCREEN.screen && INITIAL_SCREEN.screen.length) {
      this.props.navigation.navigate(
        INITIAL_SCREEN.screen,
        INITIAL_SCREEN.params
      );
    }
  };

  render() {
    return (
      <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
        <Text>Loading...</Text>
      </View>
    );
  }
}

// ...

Final Code

After all that, we're left with the following:

App.js

import React from 'react';
import {
  StyleSheet,
  Text,
  View,
  TouchableOpacity,
  Button,
  AsyncStorage,
} from 'react-native';
import {
  createAppContainer,
  createStackNavigator,
  createBottomTabNavigator,
  createSwitchNavigator,
} from 'react-navigation';

const Screen = ({ title, navigation, nextScreen, nextScreenData }) => (
  <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
    <Text style={{ fontSize: 20 }}>{title}</Text>
    {nextScreen && (
      <Button
        title={`Go to ${nextScreen}`}
        onPress={() => navigation.navigate(nextScreen, nextScreenData)}
      />
    )}
    {navigation.state && navigation.state.params && (
      <Text>{JSON.stringify(navigation.state.params, null, 2)}</Text>
    )}
  </View>
);

const App = createStackNavigator(
  {
    Tabs: createBottomTabNavigator({
      Home: {
        screen: createStackNavigator({
          Stations: {
            screen: (props) => (
              <Screen
                title="Stations"
                nextScreen="Vehicles"
                nextScreenData={{ stationId: 14 }}
                {...props}
              />
            ),
            navigationOptions: {
              headerTitle: 'Stations',
            },
          },
          Vehicles: {
            screen: (props) => (
              <Screen
                title="Vehicles"
                nextScreen="Locations"
                nextScreenData={{ vehicleId: 1401 }}
                {...props}
              />
            ),
            navigationOptions: {
              headerTitle: 'Stations',
            },
          },
          Locations: {
            screen: (props) => (
              <Screen
                title="Locations"
                nextScreen="Tools"
                nextScreenData={{ locationId: 123 }}
                {...props}
              />
            ),
            navigationOptions: {
              headerTitle: 'Stations',
            },
          },
          Tools: {
            screen: (props) => <Screen title="Tools" {...props} />,
            navigationOptions: {
              headerTitle: 'Stations',
            },
          },
        }),
      },
      Profile: {
        screen: (props) => (
          <Screen title="Profile" nextScreen="Auth" {...props} />
        ),
      },
    }),
  },
  {
    mode: 'modal',
    headerMode: 'none',
  }
);

const Auth = createStackNavigator({
  SignIn: {
    screen: (props) => (
      <Screen title="Sign In" nextScreen="SignUp" {...props} />
    ),
    navigationOptions: {
      headerTitle: 'Sign In',
    },
  },
  SignUp: {
    screen: (props) => <Screen title="Sign Up" nextScreen="App" {...props} />,
    navigationOptions: {
      headerTitle: 'Sign Up',
    },
  },
});

const demoRoutesAndParams = {
  Vehicles: {
    screen: 'Vehicles',
    params: {
      stationId: 14,
    },
  },
  Tools: {
    screen: 'Tools',
    params: {
      locationId: 123,
    },
  },
};

const INITIAL_SCREEN = demoRoutesAndParams.Tools;

class Entry extends React.Component {
  componentDidMount() {
    // Do your normal auth checking and navigation here.

    this.navigateToDevelopmentScreen();
  }

  navigateToDevelopmentScreen = () => {
    if (__DEV__ && INITIAL_SCREEN.screen && INITIAL_SCREEN.screen.length) {
      this.props.navigation.navigate(
        INITIAL_SCREEN.screen,
        INITIAL_SCREEN.params
      );
    }
  };

  render() {
    return (
      <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
        <Text>Loading...</Text>
      </View>
    );
  }
}

const RootNavigation = createSwitchNavigator({
  Entry: {
    screen: Entry,
  },
  Auth: {
    screen: Auth,
  },
  App: {
    screen: App,
  },
});

export default createAppContainer(RootNavigation);

Now what are you going to do with all that new free time?!

React Native School Logo

React Native School

Want to further level up as a React Native developer? Join React Native School! You'll get access to all of our classes and our private Slack community.