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.