Updated March 20, 2019

Best Practices for Capturing User Feedback in React Native

I've got a pet peeve - when apps ask for feedback/a review at bad times/in bad ways:

  • When I first open the app
  • In the middle of an important action (like checking out)
  • They hijack my screen and don't let me do anything unless I do their feedback thing

Thanks to Instabug for sponsoring this lesson! If you're looking for an all-in-one solution to capture user feedback, bug reports, crash reports, and more check them out! I'll have more info on how you could use them in the context of capture user feedback later in the lesson.

Good Times & Ways to Ask for Feedback

Before we get into the hands on part, lets just drop a few times/places you could ask for feedback and get good responses:

  1. When the user completes an important action (like checking out). This means detecting when an action is complete and not just asking for feedback after a given amount of time.
  2. When they take a similar action many times. Say your app gives them a random joke. Say they've pressed the "New Joke" button 15 times. That would be a good opportunity to ask why they aren't rolling on the floor gasping for air from your quality jokes you jacked from the latest Netflix stand up special.
  3. When they're vigorously shaking their phone out of frustration. Maybe ask if they're having issues? I mean how often do you see people shaking their phone out of joy?
Phone Tossing, Newest Olympic Sport in 2020

Navigation Setup for Capturing User Feedback

What's this look like in practice? Let's look at it.

The Example App

First we'll set up our example app. It's a revolutionary new app called Clargue that will, on demand, generate a random color for you. Here's the code we're starting with.

App.js

import React from 'react';
import { Text, View, Button, AsyncStorage } from 'react-native';
import randomColor from 'random-color';
import {
  createStackNavigator,
  createAppContainer,
  createSwitchNavigator,
} from 'react-navigation';

class RandomColor extends React.Component {
  state = {
    backgroundColor: randomColor().hexString(),
  };

  newColor = () => {
    this.setState((state) => ({
      backgroundColor: randomColor().hexString(),
    }));
  };

  signOut = () => {
    AsyncStorage.removeItem('isOnboarded');
    this.props.navigation.navigate('Onboarding');
  };

  render() {
    return (
      <View
        style={{
          flex: 1,
          alignItems: 'center',
          justifyContent: 'center',
          backgroundColor: this.state.backgroundColor,
        }}
      >
        <View
          style={{
            backgroundColor: 'rgba(255, 255, 255, 0.5)',
            borderRadius: 10,
            paddingHorizontal: 10,
            paddingVertical: 5,
          }}
        >
          <Text
            style={{
              fontSize: 20,
              color: '#000',
            }}
          >
            {this.state.backgroundColor}
          </Text>
        </View>
        <Button title="New Color" onPress={this.newColor} />
        <Button title="Sign Out" onPress={this.signOut} />
      </View>
    );
  }
}

class Entry extends React.Component {
  componentDidMount() {
    AsyncStorage.getItem('isOnboarded').then((isOnboarded) => {
      if (isOnboarded == 'true') {
        this.props.navigation.navigate('Main');
      } else {
        this.props.navigation.navigate('Onboarding');
      }
    });
  }

  render() {
    return null;
  }
}

const Onboard1 = ({ navigation }) => (
  <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
    <Text style={{ fontSize: 30, marginBottom: 10 }}>Welcome to Clargue</Text>
    <Text>The Biggest Innovation in</Text>
    <Text>Random Color Generation</Text>
    <Button
      title="Learn More"
      onPress={() => navigation.navigate('Onboard2')}
    />
  </View>
);

const Onboard2 = ({ navigation }) => (
  <View
    style={{
      flex: 1,
      alignItems: 'center',
      justifyContent: 'center',
      paddingHorizontal: 40,
    }}
  >
    <Text style={{ marginBottom: 30 }}>
      We use big data and machine learning to get money from investors...
    </Text>
    <Text>and a basic algorithm from Github to select your colors.</Text>
    <Button
      title="Get Started"
      onPress={() => {
        AsyncStorage.setItem('isOnboarded', 'true');
        navigation.navigate('Main');
      }}
    />
  </View>
);

const Navigator = createSwitchNavigator({
  Entry: {
    screen: Entry,
  },
  Onboarding: createStackNavigator(
    {
      Onboard1: {
        screen: Onboard1,
      },
      Onboard2: {
        screen: Onboard2,
      },
    },
    {
      headerMode: 'none',
    }
  ),
  Main: {
    screen: RandomColor,
  },
});

export default createAppContainer(Navigator);
App demo

The Feedback Screen

You can present the request for feedback/bug report/review in a variety of ways. I think the easiest is going to be by adding a global modal screen that you can access and present from anywhere in your app. For our app I'll do this by creating a new screen (which in a real situation would likely have a form of some sort) and then adding a stack navigator as our new root navigator.

App.js

// ...

const Navigator = createSwitchNavigator({
  Entry: {
    screen: Entry,
  },
  Onboarding: createStackNavigator(
    {
      Onboard1: {
        screen: Onboard1,
      },
      Onboard2: {
        screen: Onboard2,
      },
    },
    {
      headerMode: 'none',
    }
  ),
  Main: {
    screen: RandomColor,
  },
});

const FeedbackForm = () => (
  <View style={{ flex: 1, justifyContent: 'center', paddingHorizontal: 40 }}>
    <Text>
      How are you liking Clargue? We'll use your feedback to file an issue on
      the open source library - but don't worry! We'll say it's a priority
      because our business depends on a fix!
    </Text>
  </View>
);

const Modals = createStackNavigator(
  {
    Navigator: {
      screen: Navigator,
    },
    Feedback: {
      screen: createStackNavigator({
        FeedbackForm: {
          screen: FeedbackForm,
          navigationOptions: ({ navigation }) => ({
            headerTitle: 'Feedback',
          }),
        },
      }),
    },
  },
  {
    headerMode: 'none',
    mode: 'modal',
  }
);

export default createAppContainer(Modals);

With this setup we can now open the Feedback screen from anywhere in our app via this.props.navigation.navigate('Feedback') and it will appear over top of the current screen.

Presenting Feedback Form after a Given Number of Actions

We'll just cover one of the scenarios I outlined before: requesting feedback after a given number of actions.

We'll just do it based on actions in a single component but you could do this on a larger scale. Say you want to ask for feedback after 20 total actions instead of 5 single actions. You could track the number of actions in Redux/Context/whatever and as that value hits the desired number prompt them (just don't do it if they're doing something important)!

All I'm going to do is increment a clickCount on state whenever they take an action. When it hits my desired number it will reset the number and open the feedback screen.

App.js

// ...

class RandomColor extends React.Component {
  state = {
    backgroundColor: randomColor().hexString(),
    clickCount: 0,
  };

  newColor = () => {
    if (this.state.clickCount > 5) {
      this.props.navigation.navigate('Feedback');
      this.setState({ clickCount: 0 });
    } else {
      this.setState((state) => ({
        backgroundColor: randomColor().hexString(),
        clickCount: state.clickCount + 1,
      }));
    }
  };

  // ...
}

// ...
Feedback screen popping up

Give the User a Way Out

Finally, don't forget to give the user a clear way to get out of submitting feedback! Maybe that don't have the time, don't want to, or just don't know WTF is going on. Make it clear to get out. Leave a good impression.

Remember, you're building an app that provides value to them. Feedback/reviews will come eventually. Don't force it.

That said, let's add a close button to the header.

App.js

const Modals = createStackNavigator(
  {
    Navigator: {
      screen: Navigator,
    },
    Feedback: {
      screen: createStackNavigator({
        FeedbackForm: {
          screen: FeedbackForm,
          navigationOptions: ({ navigation }) => ({
            headerTitle: 'Feedback',
            headerRight: (
              <Button title="Close" onPress={() => navigation.pop()} />
            ),
          }),
        },
      }),
    },
  },
  {
    headerMode: 'none',
    mode: 'modal',
  }
);

export default createAppContainer(Modals);
Feedback screen with close button

Making Life Easier: Outsourcing Feedback & Bug Report Capturing

Obviously this is just one part of capturing feedback and bug reports. You also need to setup something on the backend to capture that feedback and present it to developers/product owners. That's more work for you and more code for you to maintain.

I like to keep my focus on the product I'm building, not so much the support system that keep things going. Capturing feedback and bug reporting is one of those things that's necessary but I don't want to build.

That's why I reach for tools like Instabug to handle that for me. Not only can you capture user feedback and bug reports but also use it to detect production exceptions and crashes - an absolutely critical piece of the puzzle for any production app.

I've been happy with their service - it works when I need it and stays out of the way the rest of the time.

If you're interest in seeing how to actually integrate and use their service check out the How to Debug React Native Apps in Development and Production class here on React Native School!

Links & Resources

Final Code

App.js

import React from 'react';
import { Text, View, Button, AsyncStorage } from 'react-native';
import randomColor from 'random-color';
import {
  createStackNavigator,
  createAppContainer,
  createSwitchNavigator,
} from 'react-navigation';

class RandomColor extends React.Component {
  state = {
    backgroundColor: randomColor().hexString(),
    clickCount: 0,
  };

  newColor = () => {
    if (this.state.clickCount > 5) {
      this.props.navigation.navigate('Feedback');
      this.setState({ clickCount: 0 });
    } else {
      this.setState((state) => ({
        backgroundColor: randomColor().hexString(),
        clickCount: state.clickCount + 1,
      }));
    }
  };

  signOut = () => {
    AsyncStorage.removeItem('isOnboarded');
    this.props.navigation.navigate('Onboarding');
  };

  render() {
    return (
      <View
        style={{
          flex: 1,
          alignItems: 'center',
          justifyContent: 'center',
          backgroundColor: this.state.backgroundColor,
        }}
      >
        <View
          style={{
            backgroundColor: 'rgba(255, 255, 255, 0.5)',
            borderRadius: 10,
            paddingHorizontal: 10,
            paddingVertical: 5,
          }}
        >
          <Text
            style={{
              fontSize: 20,
              color: '#000',
            }}
          >
            {this.state.backgroundColor}
          </Text>
        </View>
        <Button title="New Color" onPress={this.newColor} />
        <Button title="Sign Out" onPress={this.signOut} />
      </View>
    );
  }
}

class Entry extends React.Component {
  componentDidMount() {
    AsyncStorage.getItem('isOnboarded').then((isOnboarded) => {
      if (isOnboarded == 'true') {
        this.props.navigation.navigate('Main');
      } else {
        this.props.navigation.navigate('Onboarding');
      }
    });
  }

  render() {
    return null;
  }
}

const Onboard1 = ({ navigation }) => (
  <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
    <Text style={{ fontSize: 30, marginBottom: 10 }}>Welcome to Clargue</Text>
    <Text>The Biggest Innovation in</Text>
    <Text>Random Color Generation</Text>
    <Button
      title="Learn More"
      onPress={() => navigation.navigate('Onboard2')}
    />
  </View>
);

const Onboard2 = ({ navigation }) => (
  <View
    style={{
      flex: 1,
      alignItems: 'center',
      justifyContent: 'center',
      paddingHorizontal: 40,
    }}
  >
    <Text style={{ marginBottom: 30 }}>
      We use big data and machine learning to get money from investors...
    </Text>
    <Text>and a basic algorithm from Github to select your colors.</Text>
    <Button
      title="Get Started"
      onPress={() => {
        AsyncStorage.setItem('isOnboarded', 'true');
        navigation.navigate('Main');
      }}
    />
  </View>
);

const Navigator = createSwitchNavigator({
  Entry: {
    screen: Entry,
  },
  Onboarding: createStackNavigator(
    {
      Onboard1: {
        screen: Onboard1,
      },
      Onboard2: {
        screen: Onboard2,
      },
    },
    {
      headerMode: 'none',
    }
  ),
  Main: {
    screen: RandomColor,
  },
});

const FeedbackForm = () => (
  <View style={{ flex: 1, justifyContent: 'center', paddingHorizontal: 40 }}>
    <Text>
      How are you liking Clargue? We'll use your feedback to file an issue on
      the open source library - but don't worry! We'll say it's a priority
      because our business depends on a fix!
    </Text>
  </View>
);

const Modals = createStackNavigator(
  {
    Navigator: {
      screen: Navigator,
    },
    Feedback: {
      screen: createStackNavigator({
        FeedbackForm: {
          screen: FeedbackForm,
          navigationOptions: ({ navigation }) => ({
            headerTitle: 'Feedback',
            headerRight: (
              <Button title="Close" onPress={() => navigation.pop()} />
            ),
          }),
        },
      }),
      navigationOptions: {
        gesturesEnabled: false,
      },
    },
  },
  {
    headerMode: 'none',
    mode: 'modal',
  }
);

export default createAppContainer(Modals);
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 courses and our private Slack community.

Learn More