Create a guided tour plugin in the admin panel
Published: 2021-02-22 08:50:48

In this tutorial I will show you how to create your own plugin on Strapi (version 3.4.6) and more precisely how to use [reactour](https://github.com/elrumordelaluz/reactour) to create a guided tour which can be very useful for content managers out there who will have to master Strapi's admin to manage their content.

We implemented this guided tour in the admin of our official demo: [Foodadvisor](https://github.com/strapi/foodadvisor). You can try it right [here](https://strapi.io/demo).

## Create a Strapi project

Let's start by creating or simply taking an existing Strapi project. For my part, I will create a new one.

- Create a new project using the following command:

```bash
# Using yarn
yarn create strapi-app my-project --quickstart

# Using npx
npx create-strapi-app my-project --quickstart
```

- Wait for installation to end and then create your administrator.

Now we simply want to generate a new plugin for the guided tour.

- Generate a new plugin with the following command:

```bash
cd my-project
yarn strapi generate:plugin guided-tour
```

You should have this in your terminal:

```bash
strapi generate:guided-tour plugin
[...] info Generated a new plugin `guided-tour` at`. / plugins`.
```

- Add reactour to your `package.json` at the root of your Strapi project:

```bash
yarn add reactour
```

Perfect now you can start your server in watch-admin mode to customize the administration panel as your application will have the autoReload enabled.

- Start you server in watch-admin mode by running the following command:

```bash
yarn develop --watch-admin
```

## Override some admin files

To make this work, we need to override some admin files. We simply need to create an `./admin/src` folder and to override the necessary files.
**Note:** The files we are going to create will arrive in the next major of Strapi, I will not delve deeper into these files because it can get too technical but remember that it is to anticipate the arrival of the next major.

- Create a `./admin/src/containers/PrivateRoute/index.js` file with the following code:

```js
/**
 *
 * PrivateRoute
 * Higher Order Component that blocks navigation when the user is not logged in
 * and redirect the user to login page
 *
 * Wrap your protected routes to secure your container
 */

import React, { memo } from 'react';
import { Redirect, Route } from 'react-router-dom';
import PropTypes from 'prop-types';
import { auth, useStrapi } from 'strapi-helper-plugin';

/* eslint-disable react/jsx-curly-newline */

const PrivateRoute = ({ component: Component, path, ...rest }) => {
  const strapi = useStrapi();
  return (
    <Route
      path={path}
      render={(props) =>
        auth.getToken() !== null ? (
          <Component {...rest} {...props} strapi={strapi} />
        ) : (
          <Redirect
            to={{
              pathname: '/auth/login',
            }}
          />
        )
      }
    />
  );
};

PrivateRoute.propTypes = {
  component: PropTypes.oneOfType([PropTypes.node, PropTypes.func]).isRequired,
  path: PropTypes.string.isRequired,
};

export default memo(PrivateRoute);
```

- Create a `./admin/src/utils/MiddlewareApi.js` file with the following code:

```bash
import { cloneDeep } from 'lodash';

class MiddlewaresHandler {
  middlewares = [];

  add(middleware) {
    this.middlewares.push(middleware);
  }

  get middlewares() {
    return cloneDeep(this.middlewares);
  }
}

export default () => new MiddlewaresHandler();
```

- Create a `./admin/src/utils/Plugin.js` file with the following code:

```js
class Plugin {
  pluginId = null;

  decorators = {};

  injectedComponents = {};

  apis = {};

  constructor(pluginConf) {
    this.pluginId = pluginConf.id;
    this.decorators = pluginConf.decorators || {};
    this.injectedComponents = pluginConf.injectedComponents || {};
    this.apis = pluginConf.apis || {};
  }

  decorate(compoName, compo) {
    if (this.decorators && this.decorators[compoName]) {
      this.decorators[compoName] = compo;
    }
  }

  getDecorator(compoName) {
    if (this.decorators) {
      return this.decorators[compoName] || null;
    }

    return null;
  }

  getInjectedComponents(containerName, blockName) {
    try {
      return this.injectedComponents[containerName][blockName] || {};
    } catch (err) {
      console.error('Cannot get injected component', err);

      return err;
    }
  }

  injectComponent(containerName, blockName, compo) {
    try {
      this.injectedComponents[containerName][blockName].push(compo);
    } catch (err) {
      console.error('Cannot inject component', err);
    }
  }
}

export default (pluginConf) => new Plugin(pluginConf);
```

- Create a `./admin/src/utils/Strapi.js` file with the following code:

```js
import ComponentApi from './ComponentApi';
import FieldApi from './FieldApi';
import MiddlewareApi from './MiddlewareApi';
import PluginHandler from './Plugin';

class Strapi {
  componentApi = ComponentApi();

  fieldApi = FieldApi();

  middlewares = MiddlewareApi();

  plugins = {
    admin: PluginHandler({
      id: 'admin',
      injectedComponents: {
        admin: {
          onboarding: [
            // { name: 'test', Component: () => 'coming soon' }
          ],
        },
      },
    }),
  };

  getPlugin = (pluginId) => {
    return this.plugins[pluginId];
  };

  registerPlugin = (pluginConf) => {
    if (pluginConf.id) {
      this.plugins[pluginConf.id] = PluginHandler(pluginConf);
    }
  };
}

export default () => {
  return new Strapi();
};
```

- Create a `./admin/src/app.js` file with the following code:

```js
/* eslint-disable */

import '@babel/polyfill';
import 'sanitize.css/sanitize.css';

// Third party css library needed
import 'bootstrap/dist/css/bootstrap.css';
import 'font-awesome/css/font-awesome.min.css';
import '@fortawesome/fontawesome-free/css/all.css';
import '@fortawesome/fontawesome-free/js/all.min.js';

import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { BrowserRouter } from 'react-router-dom';
// Strapi provider with the internal APIs
import { StrapiProvider } from 'strapi-helper-plugin';
import { merge } from 'lodash';
import Fonts from './components/Fonts';
import { freezeApp, pluginLoaded, unfreezeApp, updatePlugin } from './containers/App/actions';
import { showNotification } from './containers/NotificationProvider/actions';
import { showNotification as showNewNotification } from './containers/NewNotification/actions';

import basename from './utils/basename';
import injectReducer from './utils/injectReducer';
import injectSaga from './utils/injectSaga';
import Strapi from './utils/Strapi';

// Import root component
import App from './containers/App';
// Import Language provider
import LanguageProvider from './containers/LanguageProvider';

import configureStore from './configureStore';
import { SETTINGS_BASE_URL } from './config';

// Import i18n messages
import { translationMessages, languages } from './i18n';

import history from './utils/history';

import plugins from './plugins';

const strapi = Strapi();

const pluginsReducers = {};
const pluginsToLoad = [];

Object.keys(plugins).forEach((current) => {
  const registerPlugin = (plugin) => {
    strapi.registerPlugin(plugin);

    return plugin;
  };
  const currentPluginFn = plugins[current];

  // By updating this by adding required methods
  // to load a plugin you need to update this file
  // strapi-generate-plugins/files/admin/src/index.js needs to be updated
  const plugin = currentPluginFn({
    registerComponent: strapi.componentApi.registerComponent,
    registerField: strapi.fieldApi.registerField,
    registerPlugin,
    settingsBaseURL: SETTINGS_BASE_URL || '/settings',
  });

  const pluginTradsPrefixed = languages.reduce((acc, lang) => {
    const currentLocale = plugin.trads[lang];

    if (currentLocale) {
      const localeprefixedWithPluginId = Object.keys(currentLocale).reduce((acc2, current) => {
        acc2[`${plugin.id}.${current}`] = currentLocale[current];

        return acc2;
      }, {});

      acc[lang] = localeprefixedWithPluginId;
    }

    return acc;
  }, {});

  // Retrieve all reducers
  const pluginReducers = plugin.reducers || {};

  Object.keys(pluginReducers).forEach((reducerName) => {
    pluginsReducers[reducerName] = pluginReducers[reducerName];
  });

  try {
    merge(translationMessages, pluginTradsPrefixed);
    pluginsToLoad.push(plugin);
  } catch (err) {
    console.log({ err });
  }
});

const initialState = {};
const store = configureStore(initialState, pluginsReducers, strapi);
const { dispatch } = store;

// Load plugins, this will be removed in the v4, temporary fix until the plugin API
// https://plugin-api-rfc.vercel.app/plugin-api/admin.html
pluginsToLoad.forEach((plugin) => {
  const bootPlugin = plugin.boot;

  if (bootPlugin) {
    bootPlugin(strapi);
  }

  dispatch(pluginLoaded(plugin));
});

// TODO
const remoteURL = (() => {
  // Relative URL (ex: /dashboard)
  if (REMOTE_URL[0] === '/') {
    return (window.location.origin + REMOTE_URL).replace(/\/$/, '');
  }

  return REMOTE_URL.replace(/\/$/, '');
})();

const displayNotification = (message, status) => {
  console.warn(
    // Validate the text
    'Deprecated: Will be deleted.\nPlease use strapi.notification.toggle(config).\nDocs : https://strapi.io/documentation/developer-docs/latest/plugin-development/frontend-development.html#strapi-notification'
  );
  dispatch(showNotification(message, status));
};
const displayNewNotification = (config) => {
  dispatch(showNewNotification(config));
};
const lockApp = (data) => {
  dispatch(freezeApp(data));
};
const unlockApp = () => {
  dispatch(unfreezeApp());
};

const lockAppWithOverlay = () => {
  const overlayblockerParams = {
    children: <div />,
    noGradient: true,
  };

  lockApp(overlayblockerParams);
};

window.strapi = Object.assign(window.strapi || {}, {
  node: MODE || 'host',
  env: NODE_ENV,
  remoteURL,
  backendURL: BACKEND_URL === '/' ? window.location.origin : BACKEND_URL,
  notification: {
    // New notification api
    toggle: (config) => {
      displayNewNotification(config);
    },
    success: (message) => {
      displayNotification(message, 'success');
    },
    warning: (message) => {
      displayNotification(message, 'warning');
    },
    error: (message) => {
      displayNotification(message, 'error');
    },
    info: (message) => {
      displayNotification(message, 'info');
    },
  },
  refresh: (pluginId) => ({
    translationMessages: (translationMessagesUpdated) => {
      render(merge({}, translationMessages, translationMessagesUpdated));
    },
    leftMenuSections: (leftMenuSectionsUpdated) => {
      store.dispatch(updatePlugin(pluginId, 'leftMenuSections', leftMenuSectionsUpdated));
    },
  }),
  router: history,
  languages,
  currentLanguage:
    window.localStorage.getItem('strapi-admin-language') ||
    window.navigator.language ||
    window.navigator.userLanguage ||
    'en',
  lockApp,
  lockAppWithOverlay,
  unlockApp,
  injectReducer,
  injectSaga,
  store,
});

const MOUNT_NODE = document.getElementById('app') || document.createElement('div');

const render = (messages) => {
  ReactDOM.render(
    <Provider store={store}>
      <StrapiProvider strapi={strapi}>
        <Fonts />
        <LanguageProvider messages={messages}>
          <BrowserRouter basename={basename}>
            <App store={store} />
          </BrowserRouter>
        </LanguageProvider>
      </StrapiProvider>
    </Provider>,
    MOUNT_NODE
  );
};

if (module.hot) {
  module.hot.accept(['./i18n', './containers/App'], () => {
    ReactDOM.unmountComponentAtNode(MOUNT_NODE);

    render(translationMessages);
  });
}

if (NODE_ENV !== 'test') {
  render(translationMessages);
}

export { dispatch };

if (window.Cypress) {
  window.__store__ = Object.assign(window.__store__ || {}, { store });
}
```

-  Create a `./admin/src/configureStore.js` file with the following code:

```js
/**
 * Create the store with dynamic reducers
 */

import { createStore, applyMiddleware, compose } from 'redux';
import { fromJS } from 'immutable';
// import { routerMiddleware } from 'react-router-redux';
import createSagaMiddleware from 'redux-saga';
import createReducer from './reducers';

const sagaMiddleware = createSagaMiddleware();

export default function configureStore(initialState = {}, reducers, strapi) {
  // Create the store with two middlewares
  // 1. sagaMiddleware: Makes redux-sagas work
  // 2. routerMiddleware: Syncs the location/URL path to the state
  const middlewares = [sagaMiddleware];

  strapi.middlewares.middlewares.forEach((middleware) => {
    middlewares.push(middleware());
  });

  const enhancers = [applyMiddleware(...middlewares)];

  // If Redux DevTools Extension is installed use it, otherwise use Redux compose
  /* eslint-disable no-underscore-dangle */
  const composeEnhancers =
    process.env.NODE_ENV !== 'production' &&
    typeof window === 'object' &&
    window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
      ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({
          // TODO Try to remove when `react-router-redux` is out of beta, LOCATION_CHANGE should not be fired more than once after hot reloading
          // Prevent recomputing reducers for `replaceReducer`
          shouldHotReload: false,
          name: 'Strapi - Dashboard',
        })
      : compose;
  /* eslint-enable */

  const store = createStore(
    createReducer(reducers),
    fromJS(initialState),
    composeEnhancers(...enhancers)
  );

  // Extensions
  store.runSaga = sagaMiddleware.run;
  store.injectedReducers = {}; // Reducer registry
  store.injectedSagas = {}; // Saga registry

  // Make reducers hot reloadable, see http://mxs.is/googmo
  /* istanbul ignore next */
  if (module.hot) {
    module.hot.accept('./reducers', () => {
      store.replaceReducer(createReducer(store.injectedReducers));
    });
  }

  return store;
}
```

## Create the tour

Now it's time to work in the guided tour! **Following instructions** will happen in the `./plugins/guided-tour-admin/src` folder.

- Dive in the guided tour folder by running the following command:

```bash
cd ./plugins/guided-tour-admin/src
```

- Update the existing `index.js` file with the following code:

```js
import pluginPkg from '../../package.json';
import pluginId from './pluginId';
import Tour from './components/Tour';
import trads from './translations';

export default (strapi) => {
  const pluginDescription = pluginPkg.strapi.description || pluginPkg.description;
  const icon = pluginPkg.strapi.icon;
  const name = pluginPkg.strapi.name;

  const plugin = {
    description: pluginDescription,
    icon,
    id: pluginId,
    initializer: null,
    isReady: true,
    isRequired: false,
    mainComponent: null,
    name,
    preventComponentRendering: false,
    trads,
    boot(app) {
      app.getPlugin('admin').injectComponent('admin', 'onboarding', {
        name: 'guided-tour',
        Component: Tour,
      });
    },
  };

  return strapi.registerPlugin(plugin);
};
```

- Remove the `containers` folder by running the following command and create the new architecture for your plugin:

```bash
rm -rf containers
mkdir -p components/Tour/utils
cd components/Tour
touch index.js reducer.js
touch utils/tour.js
```

Perfect! You have all the necessary files. Now it's time to create the logic of the tour. 

We wanted to create a tour where steps are organized by **plugins**. In fact, if you open the guided tour in the homepage then the tour will start at the very beginning. However if you open the tour in the Content-Types builder we want to open the tour at a certain step, actually, the beginning of the Content-Types Builder steps which make sense as you don't want to start from the very beginning from there.

To achieve this, we need to first define how we structure our tour. Easy to do, it will simply be a javascript object organizing steps depending on plugins.

- Update the `utils/tour.js` with the following:

```js
import React from 'react'

const tour = {
  'admin': {
    steps: [{
      selector: 'a[href="/admin"]',
      content: () => (
        <div>
        <h1>Hi! 👋 </h1><br />
          <h4>Welcome to the official demo of Strapi: <strong>Foodadvisor!</strong></h4><br />
          What about following this little guided tour to learn more about our product? Ready? <strong>Let's go!</strong><br /><br />
          (Use arrow keys to change steps)
        </div>
      )
    },
    {
      selector: 'a[href="/admin/plugins/content-type-builder"]',
      content: () => (
        <div>
        <h1>Content Types Builder</h1><br />
          This is the most important tool, the one that allows you to <strong>create the architecture of your project</strong>.<br /><br />
          <ul>
            <li>
              <strong>Click</strong> on the link to be redirected to the Content Types Builder.
            </li>
          </ul>
        </div>
      )
    }],
  },
  'content-type-builder': {
    steps: [
      {
        selector: 'a[href="/admin"]',
        content: () => (
          <div>
          <h1>Content Types Builder</h1><br />
          Welcome to the CTB! This is where you create your <strong>collection types</strong>, <strong>single types</strong> and <strong>components</strong>.<br /><br />
          Let's see how it's working here!
          </div>
        ),
      },
      {
        selector: 'div.col-md-3',
        content: () => (
          <div>
          <h1>Manage your Content Types</h1><br />
          In this sidebar you can browse your <strong>collection types</strong>, <strong>single types</strong>, <strong>components</strong> and also create new ones!<br /><br />
          Let's see one specific collection type!
          </div>
        ),
      },
    ],
  }
};

export default tour;
```

As you can see, this is a `tour` object containing an `admin` object which itself contains steps and you also have another `content-type-builder` object containing steps too.

This way we completely associate steps to specific plugins. However `admin` is not a plugin. Well this is when you'll be on the homepage, settings page etc...

**Great,** now let's build the logic to make our guided-tour works.

- Update the `Tour/index.js` file with the following code:

```js
import React, { memo, useEffect, useCallback, useReducer, useMemo } from 'react';
import reducer, { initialState } from './reducer';
import { useRouteMatch } from 'react-router-dom';
import { Button } from '@buffetjs/core';
import ReactTour from 'reactour';
import { get } from 'lodash';

const Tour = () => {
  // Get the current plugin name => pluginId
  const match = useRouteMatch('/plugins/:pluginId');
  const pluginId = get(match, ['params', 'pluginId'], 'admin');

  // Use the usereducer hook to manage our state. See reducer.js file
  const [{ isOpen, tour, actualPlugin, currentStep, totalLength }, dispatch] = useReducer(
    reducer,
    initialState
  );

  // Called when we click on the guided-tour button.
  // Change the isOpen state variable. See TOGGLE_IS_OPEN action type in reducer.js file.
  const handleClick = useCallback(() => {
    dispatch({ type: 'TOGGLE_IS_OPEN', pluginId });
  }, []);

  // Calculate the steps from the tour. See utils/tour.js file.
  const steps = useMemo(() => {
    return Object.values(tour).reduce((acc, current) => {
      return [...acc, ...current.steps];
    }, []);
  }, [tour]);

  // Main logic of the tour.
  useEffect(() => {
    let totalSteps = 0;
    const keys = Object.keys(tour);
    const previousPlugins = keys.slice(0, keys.indexOf(pluginId));
    if (previousPlugins.length > 0) {
      previousPlugins.forEach((plugin, i) => {
        totalSteps += tour[plugin].steps.length;
      });
    }
    if (tour[pluginId] && pluginId !== actualPlugin)
      dispatch({ type: 'SETUP', pluginId, totalSteps });
  }, [tour, pluginId, actualPlugin]);

  const handleNextStep = () => {
    if (tour[pluginId] && currentStep === totalLength - 1 && totalLength > 0) {
      return;
    } else if (tour[pluginId]) {
      dispatch({ type: 'NEXT_STEP', length: tour[pluginId].steps.length });
    }
  };

  return (
    <>
      {tour[pluginId] && (
        <Button
          onClick={handleClick}
          color="primary"
          style={{ right: '70px', bottom: '15px', position: 'fixed', height: '37px' }}
        >
          Guided Tour
        </Button>
      )}
      <ReactTour
        isOpen={tour[pluginId] ? isOpen : false}
        onRequestClose={handleClick}
        steps={steps}
        startAt={currentStep}
        goToStep={currentStep}
        nextStep={handleNextStep}
        prevStep={() => dispatch({ type: 'PREV_STEP' })}
        showNavigation={false}
        rounded={2}
      />
    </>
  );
};

export default memo(Tour);
```

I agree that's a lot! but don't worry, I will explain everything in this file.

```js
import React, { memo, useEffect, useCallback, useReducer, useMemo } from 'react';
import reducer, { initialState } from './reducer';
import { useRouteMatch } from 'react-router-dom';
import { Button } from '@buffetjs/core';
import ReactTour from 'reactour';
import { get } from 'lodash';
```

First we import everything we'll need to make it work. Hooks, our reducer *(reducer.js)* file that we didn't updated yet, `useRouteMatch` hook attempts to match the current URL in the same way that a `<Route>` would just to get the current plugin we're in, the Button component from Buffet.js, reactour and finally lodash.

Then in the `Tour` component we have:

```js
  const match = useRouteMatch('/plugins/:pluginId');
  const pluginId = get(match, ['params', 'pluginId'], 'admin');
```

It will allow us to get the current plugin name in the `pluginId` variable.

```js
const [{ isOpen, tour, actualPlugin, currentStep, totalLength }, dispatch] = useReducer(
  reducer,
  initialState
);
```

Our state defined by the `useReducer` hook.

```js
const handleClick = useCallback(() => {
  dispatch({ type: 'TOGGLE_IS_OPEN', pluginId });
}, []);
```

This will be called when we'll click on the guided tour button to change the value of the `isOpen` state variable.

```js
const steps = useMemo(() => {
  return Object.values(tour).reduce((acc, current) => {
    return [...acc, ...current.steps];
  }, []);
}, [tour]);
```

steps array that will contains the steps depending on our `utils/tour.js` file.

```js
useEffect(() => {
  let totalSteps = 0;
  const keys = Object.keys(tour);
  const previousPlugins = keys.slice(0, keys.indexOf(pluginId));
  if (previousPlugins.length > 0) {
    previousPlugins.forEach((plugin, i) => {
      totalSteps += tour[plugin].steps.length;
    });
  }
  if (tour[pluginId] && pluginId !== actualPlugin)
    dispatch({ type: 'SETUP', pluginId, totalSteps });
}, [tour, pluginId, actualPlugin]);
```

The main logic of the tour. This will define where the tour should start depending on where you start it.

```js
const handleNextStep = () => {
  if (tour[pluginId] && currentStep === totalLength - 1 && totalLength > 0) {
    return;
  } else if (tour[pluginId]) {
    dispatch({ type: 'NEXT_STEP', length: tour[pluginId].steps.length });
  }
};
```

-  Override of the `nexStep` function of reactour to create a behaviour like: if you need to make the user change location to another plugin then you can prevent the user to go to the next steps.

```js
return (
  <>
    {tour[pluginId] && (
      <Button
        onClick={handleClick}
        color="primary"
        style={{ right: '70px', bottom: '15px', position: 'fixed', height: '37px' }}
      >
        Guided Tour
      </Button>
    )}
    <ReactTour
      isOpen={tour[pluginId] ? isOpen : false}
      onRequestClose={handleClick}
      steps={steps}
      startAt={currentStep}
      goToStep={currentStep}
      nextStep={handleNextStep}
      prevStep={() => dispatch({ type: 'PREV_STEP' })}
      showNavigation={false}
      rounded={2}
    />
  </>
);
```

Finally we simply render the guided-tour button only if the tour contains steps for the plugin you are currently in. Then we render the reactour component with the necessary props.

- Great ! Now let's create our `utils/reducer.js` file with the following code:

```js
import produce from 'immer';
import { isEmpty, pick } from 'lodash';
import tour from './utils/tour';

const initialState = {
  tour,
  isOpen: true,
  totalLength: 0,
  currentStep: 0,
  actualPlugin: null
};

const reducer = (state, action) =>
  produce(state, draftState => {
    switch (action.type) {
      case 'TOGGLE_IS_OPEN': {
        draftState.isOpen = !state.isOpen;
        draftState.currentStep = state.currentStep;
        break;
      }
      case 'SETUP': {
        draftState.currentStep = action.totalSteps;
        draftState.actualPlugin = action.pluginId;
        draftState.totalLength = action.totalSteps + tour[action.pluginId].steps.length
        break;
      }
      case 'PREV_STEP': {
        draftState.currentStep = state.currentStep > 0 ? state.currentStep - 1 : state.currentStep;
        break;
      }
      case 'NEXT_STEP': {
        draftState.currentStep =
          state.currentStep < state.totalLength - 1 ? state.currentStep + 1 : state.currentStep;
        break;
      }
      default:
        return draftState;
    }
  });

export default reducer;
export { initialState };
```

We define here our initial state and we create behaviours depending on which action type we received:

```js
case 'TOGGLE_IS_OPEN': {
  draftState.isOpen = !state.isOpen;
  draftState.currentStep = state.currentStep;
  break;
}
```

We change the value of the isOpen to it's opposite state variable when we click on the tour button. We keep the value of the `currentStep` so that the user will go back to the step he quit the tour.


```js
case 'SETUP': {
  draftState.currentStep = action.totalSteps;
  draftState.actualPlugin = action.pluginId;
  draftState.totalLength = action.totalSteps + tour[action.pluginId].steps.length
  break;
}
```

This will setup the `currentStep, actualPlugin and totalLength` state variables every time we change plugin. These variable will not be the same depending on where you start the tour.

```js
case 'PREV_STEP': {
  draftState.currentStep = state.currentStep > 0 ? state.currentStep - 1 : state.currentStep;
  break;
}
case 'NEXT_STEP': {
  draftState.currentStep =
   state.currentStep < state.totalLength - 1 ? state.currentStep + 1 : state.currentStep;
  break;
}
```

We simply recreate the behaviour of reactour. This is necessary as we wanted to override the `nextStep` function of the package.

- Save you files and it should be ready! You should be able to have a small guided-tour with 4 steps, 2 in the `admin` and 2 in the `Content-Types Builder`.

- Now you can stop your server, build your admin and normally start your server by running the following commands at the **root** of the project:

```bash
yarn build
yarn develop
```

![guided-tour-2.png](https://assets.strapi.io/uploads/guided_tour_2_383831daef.png)
![guided-tour-1.png](https://assets.strapi.io/uploads/guided_tour_1_7c6bcea37e.png)

Thanks for for following this little tutorial! See you in another one ;)

> **Please note:** Since we initially published this blog post, we released new versions of Strapi and tutorials may be outdated. Sorry for the inconvenience if it's the case. Please help us [by reporting it here](https://github.com/strapi/community-content/issues/new/choose).

## If you want to know more 

  - [Try the Strapi live demo](https://strapi.io/demo)
  - [Starters](https://strapi.io/starters)
  - [Become a Strapi expert](https://academy.strapi.io/)
  - [Find help in our Forum](https://forum.strapi.io/)
  - [Strapi on Youtube](https://www.youtube.com/c/Strapi/featured)
  - [Try the Strapi Enterprise Edition](https://strapi.io/enterprise)