shepherd-pro/react-shepherd

How can I access the tour object?

appleneil opened this issue · 10 comments

class App extends Component {
  render() {
    return (
      <div>
        <ShepherdTour steps={newSteps} tourOptions={tourOptions}>
          <Button />
        </ShepherdTour>
      </div>
    );
}

Hey guys, how can I access the tour object to add or remove steps from within the app component?
I need to be able to change the steps according to a media query (done via matchMedia). I have tried putting steps in the state, but every time I update the state it creates a new tour.

@appleneil not sure how you're starting the tour or if you're trying to adjust steps on the fly, but const tour = useContext(ShepherdTourContext); gives you the tour instance. It's fullfilled once you start the tour. e.g., the button example in the README.

@chuckcarpenter I'm starting the tour in a function component in a separate file so I can't use useContext. I would like to access the tour instance (and add or remove steps) from within the App component (to go off the example given the in README).

public handleWindowResize = (): void => {
    if (this.mediaQuery.matches) {
      this.addStep()
    } else {
      this.removeStep()
    }
  }

Basically to do something like the above bit of code.
Without accessing the tour instance, every time I change the original steps, a new tour gets added to the DOM and so I finish up with 20 overlays in the DOM and the tour only uses the original steps provided on load.

@appleneil offhand, couldn't you useContext in the other file and pass that around? I think how you're doing it with state is causing rerender, since the tour is changing and therein kicking off a new tour. We don't share it globally, but I think you could somehow pass that in memory. Sorry, not sure without more context.

@chuckcarpenter thanks for the reply. I'm basically doing the exact same thing as the example code. I can't go through useContext in the App class component and can't find a way to access the tour instance to be able to add / remove steps cleanly. I've tried importing the context type from the button component, and static contextType = ShepherdTourContext inside the app class component but it is undefined.

@chuckcarpenter here's a basic example of what I would like to do.

import React, { Component } from 'react'
import {ShepherdTour, ShepherdTourContext} from 'react-shepherd'
import newSteps from './steps'

const tourOptions = {
  defaultStepOptions: {
    cancelIcon: {
      enabled: true
    }
  },
  useModalOverlay: true
};

function Button() {
  const tour = useContext(ShepherdTourContext);

  return (
    <button className="button dark" onClick={tour.start}>
      Start Tour
    </button>
  );
}

class App extends Component {
  mediaQuery = window.matchMedia(`(max-width: 750px)`)

  componentDidMount(): void {
    window.addEventListener('resize', this.handleWindowResize)
    this.handleWindowResize
  }

  componentWillUnmount(): void {
    window.removeEventListener('resize', this.handleWindowResize)
  }

  handleWindowResize = (): void => {
    if (this.mediaQuery.matches) {
      // TODO: add mobile step
    } else {
      // TODO: remove mobile step
    }
  }

  render() {
    return (
      <div>
        <ShepherdTour steps={newSteps} tourOptions={tourOptions}>
          <Button />
        </ShepherdTour>
      </div>
    );
}

@appleneil using class components, I believe you're going to need to use v2.1.0 of this package. https://github.com/shipshapecode/react-shepherd/tree/e4823a950d085e4eb11daa76abd7e81728dd8d67#usage

Something like:

import React, { Component } from 'react';
import { ShepherdTour, TourMethods } from 'react-shepherd';
import steps from './steps';

const tourOptions = {
  defaultStepOptions: { showCancelLink: true },
  useModalOverlay: true
};

class App extends Component {
  handleWindowResize = (): void => {
    if (this.mediaQuery.matches) {
      this.props.tourContext.addStep();
    } else {
      // TODO: remove mobile step
    }
  }

  render() {
    return (
      <div>
        <ShepherdTour steps={steps} tourOptions={tourOptions}>
          <TourMethods>
            {(tourContext) => (
              <button className="button dark" onClick={tourContext.start}>
                Start Tour
              </button>
            )}
          </TourMethods>
        </ShepherdTour>
      </div>
    );
}

Thanks for the answer but my app is too complicated to go at it this way (even if the two components involved are a class and a function component like in the example). I've been able to do what I want by adding a step based on the media query in the constructor of the app component. The only downside is if I have a user that has a device that is in between breakpoints (if he changes orientation), but it's a marginal case. Thanks for the help guys and for the great package!

@appleneil useful thread here: #317

Turns out you don't have to downgrade, so just an FYI.

I ended up rewriting the hook since it wasn't working with Next on our end:

import { useMemo } from "react";
import Shepherd from "shepherd.js";

export const useShepherdTour = ({ tourOptions, steps }) => {
  const tourObject = useMemo(() => {
    const tourInstance = new Shepherd.Tour(tourOptions);
    if (tourInstance.addSteps) {
      tourInstance.addSteps(steps);
    }
    return tourInstance;
  }, [tourOptions, steps]);

  return tourObject;
};

Adding the if (tourInstance.addSteps) { fixed the Next build issue.

@jakecyr thanks for that info. Was that an issue because of SSR render and that Tour instance creation is a noop at that point?

Also, we're going to deprecate this package soon in favor of using a monorepo and keeping things closer to the main library for better maintenance and testing. Happy to take any feedback in this PR shipshapecode/shepherd#2649

The gist will be the hook doesn't automatically return a tour instance, letting you potentially setup multiple tours much easier.