Paramanantham Harrison Web and mobile app developer. Love exploring the depth of JS full-stack development. React, Vue, React Native, Next JS, GraphQL are my current love interest. https://learnwithparam.com

Complete guide to building product tours on your React apps

8 min read 2414

An image of the React logo.

Introduction

Ever heard of tours in product UI?

An example of a preview of an Invision product tour

Product tours are self-explaining tips UI for the website user to break down complex UX and make it easily useable.

Product tours play a vital role in B2B product UI. It helps save customer support time related to repeated ‘how-to-use’ questions about the UX.

What problems do product tours solve?

Product tours help with onboarding users to new and complex UX and helps to get users familiar with UI functionalities. They’re also useful for showcasing new updates on product UI, and they can save time for the customer success team.

Slack, Trello, Asana, and Invision are some of the big products that use product tours for different UX needs.

The indirect alternative to product tours including FAQs about product functionalities, product video demos and tours, and on-demand tips UI.

However, video tours or FAQs don’t have the same level of impact as inline product tours.

The majority of users don’t look for tutorials outside the UI.

On the other hand, on-demand tips UI are similar to product tours and can have a similar impact.

In this post, you’ll learn how to build a simple product tour for your React application. Before building, it you’ll first need to learn about existing React libraries.

We made a custom demo for .
No really. Click here to check it out.

Existing React libraries for product tours

Even though product tours are used by lot of companies, there aren’t many React-based tour UIs. Some of the libraries are React Tour and React Joyride.

React Tour library

React Tour has around 1.4k stars on Github and is moderately active.

It has very nice UI if you need a simple product tour without much customization. If this is the case, React Tour UI will be good enough.

You can view the demo for React Tour here.

How it works

With React Tour, you pass the classname selector and content for each step to the component.

It will render the tour UI based on a button click, or after mounting the component. It’s simple for static pages and UI:

const steps = [
  {
    selector: '.tour-first-step',
    content: 'This is the first Step',
  },
  {
    selector: '.tour-second-step',
    content: 'This is the second Step',
  }
  // ...
]

However, if you need to customize for a custom behavior, then it won’t work very well. The component is very rigid, and styles aren’t exposed well enough to make it reusable.

One drawback is that if you don’t use styled-components in your project, then you won’t have any luck using the component. There is no other way — the library has a hard dependency for styled components.

Additionally, if a classname selector is not present in the current screen, then React Tour just displays the non-matched content in the center of the screen. There is no way to hide it.

The only way to overwrite such behavior is to trigger the next steps through our own logic, but that defeats the  purpose of the component.

It’s almost as complex as writing your own component for product tours.

React Tour really shines when you don’t want to customize a lot, and when you want basic tour functionality with beautiful UI and UX.

It also works well for static content or dynamic content where the selector labels always exist on the UI.

React Joyride library

The next famous React product tour library is React Joyride. It has 3k stars on Github and is also actively developed.

The UI isn’t as elegant as React Tours, but the API is less rigid. It allows for some level of customization.

Of course, it has its own limitations.

The docs aren’t good enough if you need custom solution on top of basic React tour functionality. The props API also isn’t very intuitive or simple.

The only difference is that it has solutions for most use cases in product tours. They expose all the events and actions to the end user, so you can capture those actions and do whatever customization you want.

Building a simple product tour in a React app

First, let’s build a simple React tour without any custom functionality.

We’ll use react-dashboard by creative tim as our base application.

This loads the product tour on top of it.

This is what the dashboard UI looks like:

An example of the UI dashboard.

We’ll do a product tour on this UI. You can see the final product tours UI here.

Let’s create the simple product tour component:

// tour.js

import React from "react";
import JoyRide from "react-joyride";

// Tour steps
const TOUR_STEPS = [
  {
    target: ".tour-search",
    content: "This is where you can search the dashboard."
  },
  {
    target: ".tour-orders",
    content:
      "Bar chart for total order. You can see beautiful graphs here, thanks to creative tim for such UI."
  },
  {
    target: ".tour-external-links",
    content: "This is where you can find the external links."
  },
  {
    target: ".tour-footer",
    content: "This is where you can see the footer links."
  }
];

// Tour component
const Tour = () => {
  return (
    <>
      <JoyRide steps={TOUR_STEPS} continuous={true} />
    </>
  );
};

export default Tour;

Load this tour component anywhere on the page to load the blinking beacon UI. If you click that beacon, it will open the tour. The next button will let you navigate til the end of the tour.

How it works

Joyride components take a lot of props. The most important ones are steps props. It accepts an array of objects with target selector elements and content.

Continuous props are used for showing the next button on each step.

You can see the demo for this simple tour component here.

Now let’s add more features and make our product tour more customized. Simple features are:

  • Skip option on each step
  • Change locale text labels
  • Hide / show buttons (next, skip, back buttons)
  • Custom styles like button colors and text alignment

Then we’ll add the custom feature like:

  • Auto start the tour
  • Start the tour by manual triggers (i.e., through link or button click)
  • Hide blinking beacon
  • Auto start tour once and only show tour on manual triggers next time

Most of the basic functionalities can be achieved through the props provided by Joyride docs.

Skip option on each step

Adding showSkipButton to true will do the trick. Skip link will skip the remaining step on the tour.

const Tour = () => {
  return (
    <>
      <JoyRide steps={TOUR_STEPS} continuous={true} showSkipButton={true} />
    </>
  );
};

How to change text labels for buttons and links

Let’s change the last button text as end tour and skip button text to close tour.

const Tour = () => {
  return (
    <>
      <JoyRide
        steps={TOUR_STEPS}
        continuous={true}
        showSkipButton={true}
        locale={{
          last: "End tour",
          skip: "Close tour"
        }}
      />
    </>
  );
};

How to hide Back, Next and Skip buttons

  • For the Skip button, use *showSkipButton* props
  • For the Back button, use hideBackButton
  • For the Next button, use continuous props

Unlike other props, continuous props work differently. They either show the Next button or show a Close button, depending on the boolean value passed to the props.

You can see how inconsistent the props API naming are. It isn’t very easy to find lot of hidden features unless you read the complete docs for Joyride couple of times 😅

Custom styles like button colors and text alignment

Styles are exposed as an object. So if you pass a style object to the component, the component will merge it with their default styles.

const Tour = () => {
  return (
    <>
      <JoyRide
        steps={TOUR_STEPS}
        continuous={true}
        showSkipButton={true}
        styles={{
          tooltipContainer: {
            textAlign: "left"
          },
          buttonNext: {
            backgroundColor: "green"
          },
          buttonBack: {
            marginRight: 10
          }
        }}
        locale={{
          last: "End tour",
          skip: "Close tour"
        }}
      />
    </>
  );
};

A caveat to this way of styling is that it only supports a handful of the object styles, which are already defined on the component.

It won’t allow you to customize everything on an element level. Also, the classnames used in the rendered elements are not very easy to customize.

However, the library exposes props to use your own elements instead of the default elements.

Some of the components are:

  • Beacon component (beaconComponent prop)
  • tooltip component (tooltipComponent prop)

Controlled product tour

So far, you’ve learned how to use the Joyride library to create a basic product tour and customize it using props.

You’ve also seen some of the limitations to styling the component.

Until now, the tour has been controlled in the library. You just pass the steps and tweak some props.

It’s possible to control the tour and trigger goto a particular step directly through button click, but it requires some coding.

We’ll see how to do it by achieving a few of the features.

The Joyride component exposes some of the actions and events through callback. You need to capture the callback and, based on the function, you can customize your functionality.

It’s simple to make the component controlled by passing a prop stepIndex.

stepIndex is the index number and starts from 0. Once you pass the values, the Next and Back button clicks need to be handled by you.

Let’s get to it. First, we will define the steps:

const TOUR_STEPS = [
  {
    target: ".tour-search",
    content: "This is where you can search the dashboard.",
    disableBeacon: true // This makes the tour to start automatically without click
  },
  {
    target: ".tour-orders",
    content:
      "Bar chart for total order. You can see beautiful graphs here, thanks to creative tim for such UI."
  },
  {
    target: ".tour-external-links",
    content: "This is where you can find the external links."
  },
  {
    target: ".tour-footer",
    content: "This is where you can see the footer links."
  },
  {
    target: ".tour-link",
    content: "This is where you can start the tour again in future."
  }
];

Here’s the initial state to make the component controlled:

// Initial state for the tour component
const INITIAL_STATE = {
  key: new Date(), // This field makes the tour to re-render when we restart the tour
  run: false,
  continuous: true, // Show next button
  loading: false,
  stepIndex: 0, // Make the component controlled
  steps: TOUR_STEPS
};

To auto start the tour, you need to pass disableBeacon: true in the first step. This will just disable the beacon. But you need to trigger start by changing the state run: true:

// Reducer will manage updating the local state
const reducer = (state = INITIAL_STATE, action) => {
  // TODO: Implement reducer  
};

// Tour component
const Tour = () => {
  // Tour state is the state which control the JoyRide component
  const [tourState, dispatch] = useReducer(reducer, INITIAL_STATE);

  useEffect(() => {
    // TODO: Auto start the tour
  }, []);

  const callback = data => {
    const { action, index, type, status } = data;
    
    // TODO: Capture close, skip, next / prev action
  };

  const startTour = () => {
    // TODO: Start the tour manually
  };

  return (
    <>
      <JoyRide
        {...tourState}
        // Callback will pass all the actions
        callback={callback}
        showSkipButton={true}
      />
    </>
  );
};

The actions that are important to make the functionality are Close button click, Skip button click, Next, and Back button click.

Let’s implement the reducer function:

// Reducer will manage updating the local state
const reducer = (state = INITIAL_STATE, action) => {
  switch (action.type) {
    // start the tour
    case "START":
      return { ...state, run: true };
    // Reset to 0th step
    case "RESET":
      return { ...state, stepIndex: 0 };
    // Stop the tour
    case "STOP":
      return { ...state, run: false };
    // Update the steps for next / back button click
    case "NEXT_OR_PREV":
      return { ...state, ...action.payload };
    // Restart the tour - reset go to 1st step, restart create new tour
    case "RESTART":
      return {
        ...state,
        stepIndex: 0,
        run: true,
        loading: false,
        key: new Date()
      };
    default:
      return state;
  }
};

Now we’ll listen to the events and dispatch proper state changes to manage the tour:

import JoyRide, { ACTIONS, EVENTS, STATUS } from "react-joyride";

// Listen to callback and dispatch state changes
const callback = data => {
    const { action, index, type, status } = data;

    if (
      // If close button clicked then close the tour
      action === ACTIONS.CLOSE ||
      // If skipped or end tour, then close the tour
      (status === STATUS.SKIPPED && tourState.run) ||
      status === STATUS.FINISHED
    ) {
      dispatch({ type: "STOP" });
    } else if (type === EVENTS.STEP_AFTER || type === EVENTS.TARGET_NOT_FOUND) {
      // Check whether next or back button click and update the step
      dispatch({
        type: "NEXT_OR_PREV",
        payload: { stepIndex: index + (action === ACTIONS.PREV ? -1 : 1) }
      });
    }
};

Here’s a quick overview of how each action, event, and state update works:

If the Close button, Skip button, or End Tour button are clicked, then STOP the tour. Meanwhile, if the Next or Back button are clicked, then check whether the target element is present in the page.

If the target element is present, then go to that step. If it’s not present, find the next step target and iterate.

Joyride expose EVENTS, STATUS, and ACTION labels. You can use those to listen to the callback event without hardcoding it.

Let’s also auto start the tour when the page loads:

useEffect(() => {
    // Auto start the tour if the tour is not viewed before
    dispatch({ type: "START" });
  }, []);

You can even trigger the start of tour using button click:

// Call startTour to start the tour
const startTour = () => {
    // Start the tour manually
    dispatch({ type: "RESTART" });
};

Right now, we have it set up so that the tour will be shown every time you refresh the page.

If you only want to show the tour once and then trigger it only through manual click, you can do so using localStorage.

You can find the working example code here and the demo here.

Steps for building a custom product tour in React

We’ve achieved the product tour using the Joyride library.

But what if we need to create our own?

Let’s walk through building a tour component.

The biggest challenges to building tour components include finding the target element and showing a popover component, as well as ensuring the popover component calculates the available window space and automatically displays by the target element.

It can also be difficult to ensure the tour component is reusable and that styles are easily extended.

To build a custom tour component in React, it’s easiest to isolate the functionality and component UI with these React Hooks:

  • useTour – a custom Hook to build your own UI on top of functionality
  • Tour – a dumb UI component that consumes useTour to load the tour portal UI

This mock code  shows how useTour works:

/*
  targetPosition - top, left position of the target element with respect to window
  gotoIndex - function that accepts stepNumber
  endTour - function to end tour
  restartTour - function to restart tour
  nextStep - function to update the state tonext step
  prevStep - function to update the state to previous step
*/

const { targetPosition, gotoIndex, currentStep, endTour, restartTour, nextStep, prevStep  } = useTour({
  steps,
  // ... props
})

Conclusion

I hope this article helped you learn the tricks of creating product tour components in your React application. Let me know your experience on tour UX in the comments 🤗

Full visibility into production React apps

Debugging React applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.

LogRocket is like a DVR for web apps, recording literally everything that happens on your React app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your React apps — .

Paramanantham Harrison Web and mobile app developer. Love exploring the depth of JS full-stack development. React, Vue, React Native, Next JS, GraphQL are my current love interest. https://learnwithparam.com

Leave a Reply