Fizl Logo

Implementing a Guided Tour with Expo [Complete Code]

Provide a unique first impression to encourage adoption and continuous use of your React Native application.

Christophe Roy | March 4, 2024

Implementing a Guided Tour with Expo [Complete Code]

The first impression can determine the success or failure of a mobile application. Thus, it's crucial to guide your users from their very first interaction with your application. By presenting a guided tour right after the completion of the signup process, the mobile application Fizl managed to increase the activation rate of its new users by 53%.

In this article, we explain step by step how to create your own guided tour using Expo, the development platform for React Native. To facilitate your implementation, the team at Fizl also provides a complete implementation in this GitHub repository.

Before you start, here's our best advice for creating your guided tour:

A well-designed guided tour is not just a tutorial; it builds an emotional connection with your users to encourage the adoption and continuous use of your mobile application.

If you need inspiration on how to build an effective guided tour, we invite you to download Fizl and experience the guided tour available to our users.

Prerequisites

To get the most out of this article, you should ideally have:

Structure of a Guided Tour

For creating our guided tour, we will implement 3 types of components: Mask, Background, and Card.

Mask Components

Mask components, composed solely of a View, play 2 important roles: defining the content to highlight and allowing the user to interact with this background content. For example, you will find the two Mask components implemented in the code provided outlined in red.

Mask

1. Defining the Content to Highlight

The content to highlight depends entirely on the experience you want to offer your users through your guided tour. However, we encourage you to centralize the style of your highlighted content to reuse it with your Mask:

/** ../styles/masks.ts */

export const MASK1_BOTTOM = 70;
export const MASK1_RIGHT = 20;
export const MASK1_HEIGHT = 70;

export const masksStyles = StyleSheet.create({
  mask1: {
    backgroundColor: "green",
    position: "absolute",
    bottom: MASK1_BOTTOM,
    right: MASK1_RIGHT,
    width: MASK1_HEIGHT,
    aspectRatio: 1,
    borderRadius: 10,
  },
  // ...masks
});

Tip: Use global variables to position your Mask, as they will be reused for Card and Background components.

2. Allowing Interactions with the Background Content

In React Native, the attribute pointerEvents="none" applied to a View indicates that it should ignore touch interactions. We use this technique to allow the user to interact with the content in the background of the Mask.

/** ../components/Masks.tsx */

import { StyleProp, View, ViewStyle } from "react-native";
import { masksStyles } from "../styles/masks";

export function Mask1({ style }: { style?: StyleProp<ViewStyle> }) {
  return <View style={[masksStyles.mask1, style]} pointerEvents="none" />;
}

Card Components

Card components are much simpler to implement and mainly serve to provide informative content and navigate through our guided tour. The main challenge you may encounter during their implementation is positioning, which must be in absolute position. For example, you will find the two Card components implemented in the code provided outlined in red.

Card

Reminder: By using global variables to position your Mask, you can easily adjust the position of your Card.

/** ../components/Cards.tsx */

const styles = StyleSheet.create({
  card1: {
    position: "absolute",
    bottom: MASK1_BOTTOM + MASK1_HEIGHT + OFFSET,
    alignSelf: "center",
  },
  // ...cards
});

Background Components

Background components are not mandatory but prevent the user from interacting with the content that is not highlighted. A simple technique used at Fizl involves covering the interface with TouchableOpacity components, except for the Mask component, to capture events.

It's also important to add the pointerEvents="box-none" attribute to the parent View component to allow touch events to pass through and reach its child components. This will enable the user to interact with the TouchableOpacity and the highlighted content behind the Mask.

/** ../components/Backgrounds.tsx */

export function Background2({ onPress }: { onPress: () => void }) {
  return (
    <View style={styles.layout} pointerEvents="box-none">
      <TouchableOpacity
        activeOpacity={1}
        onPress={onPress}
        style={{ height: MASK2_TOP }}
      />
      <Mask2 style={{ position: "relative", opacity: 0 }} />
      <TouchableOpacity
        activeOpacity={1}
        onPress={onPress}
        style={{ flex: 1 }}
      />
    </View>
  );
}

const styles = StyleSheet.create({
  layout: {
    ...StyleSheet.absoluteFillObject,
    backgroundColor: "transparent",
  },
});

Implementing Your Guided Tour

Once the structure of our guided tour is in place, we only need to centralize our components in a masked view.

What is a Masked View

A masked view is a component that controls the display of its content based on the applied mask, similar to a star-shaped stencil on a drawing, which only reveals its shape on the sheet. We will use this component from the external library @react-native-masked-view/masked-view to create a visual effect where the relevant areas of our content are highlighted through Mask.

It is advised not to use @react-native-community/masked-view as it is deprecated and incompatible with React Navigation v6.

Centralizing Our Components

Gathering the Mask, Card, and Background components of your guided tour is done in three steps:

1. Creating a Layout

The layout is a simple View that will serve as a container for your Card and your MaskedView. To achieve the desired visual result, you should assign a backgroundColor: "#000" to this parent component.

/** ../App.tsx */

import RNMaskedView from "@react-native-masked-view/masked-view";
// ...imports

export default function App() {
  // ...

  return (
    <View style={styles.layout}>
      <MaskedView />
      {index === 0 && <Card1 onNext={handleNext} />}
      {index === 1 && <Card2 onNext={handleNext} />}
    </View>
  );
}

const styles = StyleSheet.create({
  layout: {
    flex: 1,
    backgroundColor: "#000",
  },
  // ...styles
});

Note: To allow users to interact with the Card, they must be added after your MaskedView component.

2. Implementing the MaskedView

The RNMaskedView component from the @react-native-masked-view/masked-view library accepts the maskElement attribute to define the Mask applied to the child components, including your content and Background. The style backgroundColor: "#00000050" applied to the View containing your Mask controls the visibility of your background content, where an increase in opacity will make your content more visible.

/** ../App.tsx */

import RNMaskedView from "@react-native-masked-view/masked-view";
// ...imports

export default function App() {
  // ...

  function MaskedView() {
    return (
      <RNMaskedView
        style={styles.container}
        maskElement={
          <View style={styles.masks}>
            {index === 0 && <Mask1 />}
            {index === 1 && <Mask2 />}
          </View>
        }
      >
        <Content />
        {index === 0 && <Background1 onPress={onBackgroundPress} />}
        {index === 1 && <Background2 onPress={onBackgroundPress} />}
      </RNMaskedView>
    );
  }

  // ...
}

const styles = StyleSheet.create({
  // ...styles
  container: {
    backgroundColor: "#000000",
    flex: 1,
  },
  masks: {
    flex: 1,
    backgroundColor: "#00000050",
    width,
    height,
  },
});

3. Controlling Interactions

At this stage, you should already be able to visualize your work. To control your user's interactions with your guided tour, you just need to implement the onNext() and onBackgroundPress() functions. The first function, executed from your Card, is crucial for updating index, which defines the progression of your user in your guided tour.

/** ../App.tsx */

export default function App() {
  const [index, setIndex] = useState<number>(0);

  function handleNext() {
    setIndex(index + 1);
  }

  function onBackgroundPress() {
    Alert.alert("Background pressed!");
  }

  // ...
}

Optimizations

Due to their implementation, MaskedView are parent components of your content, which can lead to performance issues for complex applications. Here are two strategies used at Fizl to address this problem.

1. Display the MaskedView Only When Necessary

To minimize the impact on the performance of your application when the user is not following your guided tour, we recommend limiting the display of the MaskedView to only when necessary. Here's a simple example to achieve this goal:

/** ../App.tsx */

export default function App() {
  // ...

  return shouldRenderGuidedTour ? (
    <View style={styles.layout}>
      <MaskedView />
      {index === 0 && <Card1 onNext={handleNext} />}
      {index === 1 && <Card2 onNext={handleNext} />}
    </View>
  ) : (
    <Content />
  );
}

2. Limit the Scope of Your MaskedView

If your application implements complex navigation with @react-navigation/native-stack and @react-navigation/bottom-tabs, sharing the same MaskedView between the pages of your NativeStackNavigator and BottomTabNavigator can quickly become a challenge, not to mention the significant performance issues when changing the state of a page in your NativeStackNavigator. We recommend limiting the scope of your MaskedView and, if necessary, using a useContext to track the progression of your user on other pages.

Conclusion

A guided tour offers a unique opportunity to facilitate onboarding on your mobile application to improve the activation rate of your users. Thanks to the code provided in this GitHub repository, you have all the tools necessary to offer a personalized user experience from the opening of your mobile application.


All Posts