React Native: Dismissable Modal Stack Navigators


Posted by Youssef Moawad on: 15/01/2018, in Computer Science

Motivation

I came across this issue fairly recently. For a particular feature of my app, The Standard Model, I have to present a modal controller which would start a process that consists of multiple steps, such as create and customise a particle. On iOS, using Xcode and native components, this is fairly straightforward and storyboards make this quite easy. It also makes it easy to be able to dismiss the modal stack.

When I was working on implementing this in React Native using React Navigation however, I ran into some trouble as I couldn't find any direct way of doing this. It was fairly easy to have a stack navigator appear modally (we'll discuss how to do this) but dismissing it wasn't so direct. This is because calling goBack() as discussed in the previous tutorial acts on the screen, and in this case it tries to go back to the previous screen in the stack, and there may not be one. Either way, nothing will happen.

I started looking for ways to do this online, and after a lot of digging I came across an issue thread on the official React Navigation repo, at the end of which I found the answer. So credit for some of the code here goes to contributors to that thread, mainly richardfickling, who came up with the most accepted solution, and martnu, for adapting the solution to a newer version of React Navigation.

Carrying on from the previous post

As in the previous tutorial, I will assume that you are familiar with basics of React Native and know a bit about how React Navigation is typically implemented, which you can learn about from their website.

I will also use the end point of the previous tutorial as a starting point to save some time on creating a new app, setting up folders, etc. You can find the code from that post on its GitHub repo.

Clone the repo to carry on from the previous tutorial.

Renaming the project

Feel free to skip this section if you're not too bothered about carrying on from the same name as the previous project.

Go ahead and rename the project folder to "RNDismissableModalStacks". You will also need to change the name in the package.js file to the same name.

You now need to run these two commands from terminal in the project folder:

  $ npm upgrade
  $ npm link
  $ npm install

Make sure to run it now on the simulator and confirm that it works alright. You may need to clear the cache if it fails to load the first time.

What we'll be building

We already have an app with two screens, the second of which displays modally when the button in the first screen is pressed.

In this tutorial, we're going to extend this to have the modal screen be a stack navigator with at least two screens. We will mainly have to change our second screen in the Root object to be a StackNavigator and then have that navigator hold that second screen and another screen to navigate to.

Creating the Modal StackNavigator

To start with, let's simply create a second StackNavigatorin routes.js to act as the modal stack. Add this before the Root object declaration:

config/routes.js:ModalNavigator

  
    const ModalNavigator = StackNavigator ({
      FirstModal: { screen: ModalScreen }
    })
  

Notice that the first screen defined here is the same ModalScreen screen, which we defined and implemented in the previous tutorial.

Now, let's change our Root object to refer to that navigator as the second item, with the same key as before. Make these changes now:

config/routes.js:Root

  
    export default Root = StackNavigator({
      Main: { screen: MainNavigator },

      Modal: { screen: ModalNavigator } // Change this from ModalScreen to ModalNavigator
    },{
      mode: 'modal',
      headerMode: 'none',
    })
  

So essentially, our navigation stack will look something like this:

Diagram of the navigation stack.

Go ahead and run the app in the simulator. Not much will have changed from before. You should mainly notice that the modal screen now has a top bar, as it is a part of a StackNavigator, and that when you press the blue 'Back' button on it, it does not dismiss. As discussed above, this is because when you call goBack(), it tries to go back from the current screen in the current stack navigator. So there's nothing to go back to and the button does nothing.

Second Modal Screen in the Stack

To demonstrate this, let's add another screen to the modal stack navigator. Go ahead and create SecondModalScreen.js in the screens folder. This screen will simply have a label and two buttons: one to go back to the first modal screens and a second to entirely dismiss the modal. We won't be implementing the second button just yet.

The code in this file should be something like:

screens/SecondModalScreen.js

  
    import React from 'react';
    import { StyleSheet, Text, View, TouchableOpacity } from 'react-native';

    export default class SecondModalScreen extends React.Component {
      static navigationOptions = {
        title: 'Second Modal',
      };

      backButtonPressed() {
        this.props.navigation.goBack()
      }

      dismissButtonPressed() {

      }

      render() {
        return (
          <View style={this.styles.container}>
            <Text>{"You reached the second modal screen!"}</Text>

            <TouchableOpacity onPress={() => this.backButtonPressed()}>
              <View style={this.styles.button}>
                <Text style={{color: 'white'}}>{"Go back"}</Text>
              </View>
            </TouchableOpacity>

            <TouchableOpacity onPress={() => this.dismissButtonPressed()}>
              <View style={[this.styles.button, {backgroundColor:"#ee3131"}]}>
                <Text style={{color: 'white'}}>{"Dismiss Modal"}</Text>
              </View>
            </TouchableOpacity>

          </View>
        )
      }

      styles = StyleSheet.create({
        container: {
          flex: 1,
          backgroundColor: '#fff',
          alignItems: 'center',
          justifyContent: 'center',
        },

        button: {
          height: 45,
          width: 350,
          backgroundColor:"#0075ff",
          alignItems: 'center',
          justifyContent: 'center',
          borderRadius: 10,
          marginTop: 10,
        }
      })
    }
  

Remember to export this class from the screens folder using the index.js file:

screens/index.js

  
    export MainScreen from './MainScreen'
    export ModalScreen from './ModalScreen'
    export SecondModalScreen from './SecondModalScreen'
  

Adding the Second Screen to the Navigation Stack

Now go ahead and import the second screen into the routes.js file and add it as an entry to the ModalNavigator:

config/routes.js:imports

  
    import { MainScreen, ModalScreen, SecondModalScreen } from '../screens/';
  

config/routes.js:ModalNavigator

  
    const ModalNavigator = StackNavigator ({
      FirstModal: { screen: ModalScreen },
      SecondModal: { screen: SecondModalScreen }
    })
  

Some changes to the first modal screen

If we run the app in the simulator just now, we wouldn't be able to reach the second screen because the button in the first screen still just calls goBack(). We need this to instead navigate to "SecondModal". Go ahead and change this:

screens/ModalScreen.js:modalScreenButtonPressed

  
    modalScreenButtonPressed() {
      this.props.navigation.navigate("SecondModal")
    }
  

You should now be able to run the app in the simulator and navigate to the second modal screen by pressing the button in the first modal screen. You should get something like this:

The second modal screen.

Finally you should change your first modal screen to have an appropriate button title, and add a similar red button like the one in the second screen. The full code for the ModalScreen.js file so far should look like this:

screens/ModalScreen.js

  
    import React from 'react';
    import { StyleSheet, Text, View, TouchableOpacity } from 'react-native';

    export default class ModalScreen extends React.Component {
      static navigationOptions = {
        title: 'Modal',
      };

      modalScreenButtonPressed() {
        this.props.navigation.navigate("SecondModal")
      }

      dismissButtonPressed() {

      }

      render() {
        return (
          <View style={this.styles.container}>
            <Text>{"This is the first modal screen."}</Text>

            <TouchableOpacity onPress={() => this.modalScreenButtonPressed()}>
              <View style={this.styles.button}>
                <Text style={{color: 'white'}}>{"Go to next screen"}</Text>
              </View>
            </TouchableOpacity>

            <TouchableOpacity onPress={() => this.dismissButtonPressed()}>
              <View style={[this.styles.button, {backgroundColor:"#ee3131"}]}>
                <Text style={{color: 'white'}}>{"Dismiss Modal"}</Text>
              </View>
            </TouchableOpacity>

          </View>
        )
      }

      styles = StyleSheet.create({
        container: {
          flex: 1,
          backgroundColor: '#fff',
          alignItems: 'center',
          justifyContent: 'center',
        },

        button: {
          height: 45,
          width: 350,
          backgroundColor:"#0075ff",
          alignItems: 'center',
          justifyContent: 'center',
          borderRadius: 10,
          marginTop: 10,
        }
      })
    }
  

You should now have two functioning modal stack screens that can navigate forwards and backwards to each other. We just need to be able to actually dismiss the modal.

The important bit

What we need is to able to call the goBack() function on the StackNavigator from the actual screens. However, there's no way to actually do this right now in React Navigation by default. We have to pass down the navigator's goBack() function to the screens when creating the navigator.

To do this, richardfickling's answer suggested subclassing StackNavigator to create a DismissableStackNavigator class which would give this functionality to its contained screens. We can then use this DismissableStackNavigator in place of the default StackNavigator. So we will actually have to modify our routes file after we implement this class.

Go ahead and create a helpers folder and create two files inside: index.js, to export properly as usual, and DismissableStackNavigator.js

Here's how the implemented DismissableStackNavigator looks, after being modified for the later React Navigation version according to martnu's answer:

helpers/DismissableStackNavigator.js

  
    import React, { Component } from 'react';
    import { StackNavigator } from 'react-navigation';

    export default function DismissableStackNavigator(routes, options) {
      const StackNav = StackNavigator(routes, options);

      return class DismissableStackNav extends Component {
        static router = StackNav.router;

        render() {
          const { state, goBack } = this.props.navigation;
          const props = {
            ...this.props.screenProps,
            dismiss: () => goBack(state.key),
          };
          return (
            <StackNav
              screenProps={props}
              navigation={this.props.navigation}
            />
          );
        }
      }
    };
  

This is just usual ES6 code, so feel free to go through it to understand what is really happening here. The important thing to note here is that it lets us simply call a dismiss() function from a screenProps object which will be in the screen's props. The proper use for this will be:

  
    this.props.screenProps.dismiss()
  

Remember to export this class from the helpers folder using the index.js file, as usual:

helpers/index.js

  
    export DismissableStackNavigator from './DismissableStackNavigator'
  

Changing routes to use DismissableStackNavigator

We're now ready to go ahead and use the new DismissableStackNavigator class in our routes to give the screens this functionaliy. For starter, import it into routes.js:

config/routes.js:imports

  
    import { MainScreen, ModalScreen, SecondModalScreen } from '../screens/';
  

And now it's just a matter of changing StackNavigator to DismissableStackNavigator in the declaration of the ModalNavigator. The full code in the routes file should look like:

config/routes.js

  
    import React from 'react';
    import { StackNavigator } from 'react-navigation';

    import { MainScreen, ModalScreen, SecondModalScreen } from '../screens/';
    import { DismissableStackNavigator } from '../helpers/'

    const MainNavigator = StackNavigator({
      Main: { screen: MainScreen }
    })

    const ModalNavigator = DismissableStackNavigator({
      FirstModal: { screen: ModalScreen },
      SecondModal: { screen: SecondModalScreen }
    })

    export default Root = StackNavigator({
      Main: { screen: MainNavigator },

      Modal: { screen: ModalNavigator }
    },{
      mode: 'modal',
      headerMode: 'none',
    })
  

Using dismiss in the modal screens

The last thing now is to just modify the dismissButtonPressed() functions of the two modal screens to actually use this new functionality. Go ahead and do this like so:

screens/ModalScreen.js:dismissButtonPressed

  
    dismissButtonPressed() {
      this.props.screenProps.dismiss()
    }
  

screens/SecondModalScreen.js:dismissButtonPressed

  
    dismissButtonPressed() {
      this.props.screenProps.dismiss()
    }
  

That's it! Go ahead and run the app and you should be able to dismiss the modal stack from either screen.

Conclusion

Thanks for reading! I hope you found this tutorial interesting and helpful. If you have any suggestions or questions about the way this was implemented, please do leave a comment or feel free to send me a message using the contact me section.

I also put up the code used here on GitHub. You can find the full code for this app on the repo, right here.

Also, if you have any recommendations or requests for more tutorials, please let me know. I'm open to suggestions!

Happy Coding!

Related

You may also be interested in:

React Native: Modal Screens with React Navigation

Blog Plans for 2018


Comments