Updated March 13, 2019
Easily Work on Deeply Nested Screens in React Native
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);
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...
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>
);
}
}
// ...
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.
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?!