Реактивность и туры

Динамическая природа туров

В основе работы Shepherd.js лежит концепция пошаговых туров, однако в реальных приложениях статичная последовательность шагов оказывается недостаточной. Интерфейс может изменяться в зависимости от состояния приложения, пользовательских действий или асинхронных данных. В таких условиях требуется реактивный подход — способность тура адаптироваться к изменениям среды выполнения.

Реактивность в контексте Shepherd.js — это управление жизненным циклом шагов с учётом динамики DOM, состояния UI и логики приложения.


Управление состоянием тура

Привязка к состоянию приложения

Тур должен учитывать текущее состояние интерфейса. Например:

  • элементы могут отсутствовать в DOM
  • компоненты могут быть скрыты
  • данные могут загружаться асинхронно

Для решения этой задачи используется комбинация:

  • условий отображения (when)
  • динамического определения attachTo
  • событий (beforeShowPromise, show, hide)

Пример:

const tour = new Shepherd.Tour({
  defaultStepOptions: {
    scrollTo: true,
    cancelIcon: {
      enabled: true
    }
  }
});

tour.addStep({
  id: 'dynamic-step',
  text: 'Этот шаг зависит от наличия элемента',
  attachTo: {
    element: () => document.querySelector('.dynamic-element'),
    on: 'bottom'
  },
  beforeShowPromise: function () {
    return new Promise((resolve) => {
      const interval = setInterval(() => {
        if (document.querySelector('.dynamic-element')) {
          clearInterval(interval);
          resolve();
        }
      }, 100);
    });
  }
});

Здесь шаг не отобразится, пока элемент не появится в DOM.


Реакция на изменения DOM

Ожидание элементов

Частая ситуация — элемент появляется после рендера или загрузки данных. Shepherd.js не отслеживает DOM автоматически, поэтому необходимо реализовать ожидание вручную.

Подходы:

  1. Polling (опрос DOM) — как в примере выше
  2. MutationObserver — более эффективный способ
function waitForElement(selector) {
  return new Promise((resolve) => {
    const element = document.querySelector(selector);
    if (element) {
      return resolve(element);
    }

    const observer = new MutationObserver(() => {
      const el = document.querySelector(selector);
      if (el) {
        observer.disconnect();
        resolve(el);
      }
    });

    observer.observe(document.body, {
      childList: true,
      subtree: true
    });
  });
}

Использование:

beforeShowPromise: () => waitForElement('.async-element')

Условное отображение шагов

Пропуск шагов

Не все шаги должны отображаться всегда. Например:

  • пользователь уже знаком с функцией
  • элемент недоступен
  • условие не выполнено

Для этого используется логика внутри when.show или динамическое добавление шагов.

tour.addStep({
  id: 'conditional-step',
  text: 'Этот шаг показывается только при условии',
  when: {
    show: function () {
      if (!window.user.isNew) {
        tour.next();
      }
    }
  }
});

Альтернативный подход — формировать список шагов перед запуском:

if (user.isNew) {
  tour.addStep({ ... });
}

Реактивность во фреймворках

Интеграция с React

В React состояние интерфейса управляется через state и props. Shepherd.js работает напрямую с DOM, поэтому важно синхронизировать эти два мира.

Основные принципы:

  • запуск тура после рендера
  • использование useEffect
  • хранение экземпляра тура в useRef
import { useEffect, useRef } from 'react';
import Shepherd from 'shepherd.js';

function App() {
  const tourRef = useRef(null);

  useEffect(() => {
    tourRef.current = new Shepherd.Tour({});

    tourRef.current.addStep({
      text: 'Добро пожаловать',
      attachTo: {
        element: '.header',
        on: 'bottom'
      }
    });

    tourRef.current.start();
  }, []);

  return <div className="header">...</div>;
}

Проблема синхронизации

Если элемент появляется после асинхронного обновления:

useEffect(() => {
  if (dataLoaded) {
    tourRef.current.start();
  }
}, [dataLoaded]);

Интеграция с Vue

В Vue реактивность встроена в систему. Основная задача — дождаться обновления DOM после изменения состояния.

this.$nextTick(() => {
  tour.start();
});

Обработка пользовательских действий

Реакция на клики и события

Тур может изменяться в зависимости от действий пользователя:

tour.addStep({
  id: 'click-step',
  text: 'Нажмите на кнопку',
  attachTo: {
    element: '.btn',
    on: 'bottom'
  },
  advanceOn: {
    selector: '.btn',
    event: 'click'
  }
});

Это создаёт реактивное поведение: шаг автоматически завершается при действии пользователя.


Динамическое изменение шагов

Добавление и удаление шагов

Shepherd позволяет управлять шагами во время выполнения:

tour.addStep({ id: 'step1', text: 'Шаг 1' });

if (condition) {
  tour.addStep({ id: 'step2', text: 'Дополнительный шаг' });
}

Удаление:

tour.removeStep('step2');

Управление потоком тура

Переходы между шагами

Реактивность может выражаться в изменении маршрута тура:

tour.addStep({
  id: 'branching-step',
  text: 'Выберите действие',
  buttons: [
    {
      text: 'Вариант A',
      action: () => tour.show('stepA')
    },
    {
      text: 'Вариант B',
      action: () => tour.show('stepB')
    }
  ]
});

Так реализуется нелинейный сценарий.


Работа с асинхронными данными

beforeShowPromise

Ключевой инструмент реактивности — beforeShowPromise. Он позволяет задержать показ шага до выполнения асинхронной операции:

tour.addStep({
  id: 'async-step',
  text: 'Данные загружаются...',
  beforeShowPromise: () => {
    return fetch('/api/data')
      .then(response => response.json())
      .then(data => {
        window.data = data;
      });
  }
});

Управление видимостью элементов

Проблемы позиционирования

Если элемент скрыт (display: none), Shepherd не сможет корректно вычислить позицию.

Решения:

  • показывать элемент перед шагом
  • использовать beforeShowPromise
  • проверять размеры элемента
beforeShowPromise: () => {
  return new Promise(resolve => {
    const el = document.querySelector('.hidden');
    el.style.display = 'block';
    resolve();
  });
}

Обновление позиции шага

Если элемент перемещается (например, при анимации или ресайзе), необходимо обновить позицию:

window.addEventListener('resize', () => {
  const step = tour.getCurrentStep();
  if (step) {
    step.updateStepOptions({});
  }
});

Управление жизненным циклом

События тура

Shepherd предоставляет события:

  • start
  • show
  • hide
  • complete
  • cancel

Пример:

tour.on('show', (event) => {
  console.log('Показ шага:', event.step.id);
});

Это позволяет реагировать на изменения состояния тура.


Синхронизация с маршрутизацией

Работа с SPA

В одностраничных приложениях шаги могут зависеть от маршрута:

tour.addStep({
  id: 'route-step',
  text: 'Этот шаг на другой странице',
  beforeShowPromise: () => {
    return new Promise((resolve) => {
      router.push('/dashboard');
      setTimeout(resolve, 300);
    });
  }
});

Управление несколькими турами

Контекстная реактивность

В сложных системах может существовать несколько туров:

  • onboarding
  • feature-tour
  • help-tour

Выбор тура:

if (user.isFirstVisit) {
  onboardingTour.start();
} else {
  featureTour.start();
}

Отложенный запуск

Lazy-инициализация

Тур можно запускать при определённых условиях:

setTimeout(() => {
  if (!user.hasSeenTour) {
    tour.start();
  }
}, 2000);

Обработка ошибок

Отсутствие элементов

Если элемент не найден:

  • шаг может “сломаться”
  • тур остановится

Решение:

attachTo: {
  element: () => {
    const el = document.querySelector('.safe');
    return el || 'body';
  },
  on: 'center'
}

Практические паттерны

1. Тур после загрузки данных

  • ожидание API
  • запуск тура

2. Тур с ветвлением

  • разные сценарии
  • динамические переходы

3. Тур с ленивыми компонентами

  • ожидание рендера
  • MutationObserver

4. Тур, реагирующий на пользователя

  • события advanceOn
  • кастомные действия

Производительность и оптимизация

  • избегать частого polling
  • использовать MutationObserver
  • удалять неиспользуемые шаги
  • не запускать тур до готовности DOM

Архитектурные рекомендации

  • изолировать логику тура
  • использовать фабрики шагов
  • хранить конфигурации отдельно
  • интегрировать с состоянием приложения

Пример:

function createStep(id, selector, text) {
  return {
    id,
    text,
    attachTo: {
      element: selector,
      on: 'bottom'
    }
  };
}

Итоговая модель реактивного тура

Реактивный тур в Shepherd.js строится на сочетании:

  • динамического доступа к DOM
  • асинхронных ожиданий
  • событийной модели
  • интеграции с состоянием приложения

Такая архитектура позволяет создавать гибкие, устойчивые и адаптивные пользовательские сценарии, которые корректно работают даже в сложных SPA-интерфейсах.