Стратегия постепенной миграции

Переход на Popper.js (в актуальной версии — @popperjs/core) в существующем проекте требует аккуратного подхода, особенно если ранее использовались самописные решения позиционирования или устаревшие библиотеки. Резкая замена часто приводит к регрессиям в интерфейсе: всплывающие подсказки, выпадающие меню и контекстные окна начинают вести себя нестабильно.

Постепенная миграция строится на следующих принципах:

  • Инкрементальность — замена происходит по частям, а не целиком
  • Совместимость — новый и старый код могут временно сосуществовать
  • Изоляция — изменения локализуются в отдельных компонентах
  • Наблюдаемость — поведение UI контролируется и тестируется на каждом этапе

Аудит существующей системы позиционирования

Перед началом миграции необходимо выявить все места, где используется позиционирование элементов:

  • Tooltip (подсказки)
  • Dropdown (выпадающие списки)
  • Popover (всплывающие панели)
  • Контекстные меню
  • Кастомные модальные окна с привязкой к элементу

Типичные источники:

element.getBoundingClientRect()
window.scrollX / scrollY
position: absolute / fixed

Важно определить:

  • есть ли повторяющаяся логика
  • используются ли сторонние библиотеки (например, старые версии Popper.js)
  • как обрабатываются крайние случаи (выход за границы экрана, скролл)

Создание адаптера для Popper.js

Чтобы не переписывать весь код сразу, вводится промежуточный слой — адаптер. Он инкапсулирует работу с Popper.js и предоставляет API, похожий на существующий.

Пример:

import { createPopper } from '@popperjs/core';

export function createPositioning(reference, popper, options = {}) {
  return createPopper(reference, popper, {
    placement: options.placement || 'bottom',
    modifiers: [
      {
        name: 'offset',
        options: {
          offset: options.offset || [0, 8],
        },
      },
      {
        name: 'preventOverflow',
        options: {
          boundary: options.boundary || 'viewport',
        },
      },
    ],
  });
}

Это позволяет:

  • централизовать конфигурацию
  • упростить замену старого API
  • минимизировать изменения в компонентах

Параллельное использование старого и нового подхода

На первом этапе новые компоненты используют Popper.js, а старые продолжают работать как прежде.

Стратегия:

  1. Новые UI-элементы сразу реализуются через Popper.js
  2. Старые компоненты не трогаются
  3. Общие стили и поведение унифицируются

Пример:

if (useNewPopper) {
  createPositioning(button, tooltip);
} else {
  legacyPositioning(button, tooltip);
}

Флаг useNewPopper может быть:

  • глобальным (feature flag)
  • конфигурационным
  • зависеть от версии компонента

Миграция по типам компонентов

Переход выполняется по категориям UI-элементов:

1. Tooltip

Самый простой случай:

  • нет сложной интерактивности
  • минимальные зависимости

Пример:

createPopper(referenceEl, tooltipEl, {
  placement: 'top',
});

Учитываются:

  • динамическое изменение размеров
  • обработка кликов вне элемента
  • управление фокусом

Добавляются модификаторы:

modifiers: [
  {
    name: 'flip',
    options: {
      fallbackPlacements: ['top', 'right'],
    },
  },
]

3. Popover

Более сложная логика:

  • взаимодействие с контентом
  • изменение размеров после открытия

Используется обновление позиции:

instance.update();

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

Popper.js требует явного контроля создания и уничтожения экземпляров.

Создание:

const instance = createPopper(reference, popper);

Уничтожение:

instance.destroy();

При миграции важно:

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

Работа с модификаторами

Модификаторы — ключевая часть Popper.js. При миграции необходимо сопоставить старую логику с новыми возможностями.

Часто используемые:

  • offset — отступ
  • flip — переворот при нехватке места
  • preventOverflow — предотвращение выхода за границы
  • arrow — позиционирование стрелки

Пример:

modifiers: [
  {
    name: 'arrow',
    options: {
      element: arrowElement,
    },
  },
]

Если в старом коде была кастомная логика, её можно перенести в собственный модификатор:

const customModifier = {
  name: 'customLogic',
  enabled: true,
  phase: 'main',
  fn({ state }) {
    // кастомная обработка
  },
};

Обработка edge-case сценариев

При миграции выявляются проблемные ситуации:

  • элемент выходит за пределы viewport
  • вложенные скроллируемые контейнеры
  • изменение размеров контента после рендера

Popper.js решает это через:

  • strategy: 'fixed' | 'absolute'
  • настройку boundary
  • использование eventListeners

Пример:

createPopper(reference, popper, {
  strategy: 'fixed',
});

Интеграция с фреймворками

В проектах с React, Vue или Angular миграция требует обёрток.

Пример для React:

useEffect(() => {
  const instance = createPopper(ref.current, popper.current);

  return () => {
    instance.destroy();
  };
}, []);

Важно:

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

Тестирование и контроль качества

После каждого этапа миграции проводится проверка:

  • визуальная корректность позиционирования
  • поведение при скролле
  • адаптивность
  • работа в разных браузерах

Типы тестов:

  • ручное тестирование UI
  • snapshot-тесты
  • e2e (например, с Cypress)

Удаление устаревшего кода

После завершения миграции:

  • удаляются старые утилиты позиционирования
  • убираются feature flags
  • упрощается код компонентов

Важно убедиться, что:

  • нет скрытых зависимостей
  • все компоненты используют единый API
  • документация обновлена

Оптимизация после миграции

После полного перехода открываются дополнительные возможности:

  • уменьшение объёма кода
  • унификация поведения UI
  • повышение стабильности

Можно:

  • централизовать настройки Popper
  • внедрить собственные модификаторы
  • оптимизировать производительность через отключение лишних listeners

Пример:

modifiers: [
  {
    name: 'eventListeners',
    options: {
      scroll: false,
      resize: true,
    },
  },
]

Типичные ошибки при миграции

  • Создание нескольких Popper-инстансов для одного элемента
  • Игнорирование destroy()
  • Неправильный выбор strategy (absolute vs fixed)
  • Отсутствие обработки динамического контента
  • Попытка полностью повторить старую логику вместо использования возможностей Popper.js

Грамотная стратегия постепенной миграции позволяет избежать критических сбоев и обеспечивает контролируемый переход на современную систему позиционирования.