React компоненты

Shopify Draggable — это библиотека для управления перетаскиванием элементов в DOM, которая предоставляет высокий уровень гибкости, включая поддержку drag-and-drop, sortable списков и контейнеров с несколькими зонами перетаскивания. В контексте React ключевым аспектом является синхронизация состояния компонентов с событиями Draggable, поскольку библиотека напрямую работает с DOM, а React использует виртуальный DOM. Несогласованность этих слоев может привести к багам, таким как «мерцающие» элементы или некорректное обновление списка после сортировки.

Для корректной работы чаще всего используют рефы (useRef) для привязки Draggable к реальным DOM-элементам, а также эффекты (useEffect) для инициализации и очистки экземпляра Draggable.

import { useEffect, useRef } from 'react';
import { Draggable, Sortable } from '@shopify/draggable';

function SortableList({ items, onUpdate }) {
  const containerRef = useRef(null);

  useEffect(() => {
    const sortable = new Sortable(containerRef.current, {
      draggable: '.item'
    });

    sortable.on('sortable:stop', (event) => {
      const newIndex = event.newIndex;
      const oldIndex = event.oldIndex;
      onUpdate(oldIndex, newIndex);
    });

    return () => sortable.destroy();
  }, [onUpdate]);

  return (
    <div ref={containerRef}>
      {items.map((item, index) => (
        <div key={item.id} className="item">{item.content}</div>
      ))}
    </div>
  );
}

Использование Sortable и Draggable в React

Draggable предоставляет низкоуровневый API для управления событиями перетаскивания, а Sortable — высокоуровневый инструмент для сортируемых списков. В React обычно используют Sortable, так как он автоматически обрабатывает порядок элементов и события drag-and-drop. Основные события для интеграции:

  • sortable:start — начало перетаскивания
  • sortable:sort — изменение позиции во время drag
  • sortable:stop — завершение перетаскивания
  • sortable:updated — окончательная смена порядка элементов

Для корректного обновления состояния компонента важно внутри обработчика событий изменять только состояние React, а не напрямую DOM. Это позволяет сохранить совместимость с виртуальным DOM.

sortable.on('sortable:stop', (event) => {
  const updatedItems = Array.from(items);
  const [movedItem] = updatedItems.splice(event.oldIndex, 1);
  updatedItems.splice(event.newIndex, 0, movedItem);
  setItems(updatedItems);
});

Работа с динамическими списками

React-компоненты часто рендерят динамические списки, которые меняются в процессе работы. В таких случаях необходимо:

  1. Следить за обновлением ref контейнера. Если контейнер изменился, нужно пересоздать экземпляр Sortable.
  2. Использовать key для элементов списка, чтобы React корректно отслеживал позиции и предотвращал пересоздание элементов.
  3. Обрабатывать добавление и удаление элементов через состояние, не полагаясь на внутренние методы Draggable для изменения DOM.
useEffect(() => {
  const sortable = new Sortable(containerRef.current, {
    draggable: '.item'
  });

  return () => sortable.destroy();
}, [items.length]); // пересоздание при изменении числа элементов

Настройка ограничений и групп перетаскивания

Draggable поддерживает группы (containers), что позволяет перетаскивать элементы между несколькими списками. В React это реализуется через несколько ref:

const containerRefs = useRef([]);

useEffect(() => {
  const sortable = new Sortable(containerRefs.current, {
    draggable: '.item',
    mirror: {
      constrainDimensions: true
    }
  });

  sortable.on('sortable:stop', (event) => {
    const sourceIndex = event.oldIndex;
    const targetIndex = event.newIndex;
    onMoveBetweenLists(event.source, event.over, sourceIndex, targetIndex);
  });

  return () => sortable.destroy();
}, [lists]);

Особенности:

  • mirror.constrainDimensions позволяет сохранить размеры элемента при переносе.
  • Использование event.source и event.over помогает определить источник и цель перетаскивания между списками.
  • Для нескольких контейнеров важно корректно управлять состоянием каждого списка в React.

Анимации и визуальные эффекты

Draggable поддерживает создание зеркальных элементов (mirror) при перетаскивании. В React анимации чаще всего настраиваются через CSS или через состояние, которое меняется при событиях Draggable:

.item {
  transition: transform 0.2s ease;
}
sortable.on('sortable:sort', (event) => {
  const { dragEvent } = event;
  event.source.style.transform = `translate(${dragEvent.delta.x}px, ${dragEvent.delta.y}px)`;
});

Это позволяет синхронизировать визуальное перемещение с виртуальным DOM, предотвращая конфликты между React и библиотекой.


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

При больших списках важно:

  • Использовать React.memo или PureComponent для элементов списка.
  • Минимизировать пересоздание экземпляров Draggable при каждом рендере.
  • События Draggable обрабатывать через useCallback, чтобы избежать лишних подписок на события.
const handleUpdate = useCallback((oldIndex, newIndex) => {
  setItems((prevItems) => {
    const updated = [...prevItems];
    const [moved] = updated.splice(oldIndex, 1);
    updated.splice(newIndex, 0, moved);
    return updated;
  });
}, []);

Интеграция с формами и пользовательскими компонентами

Draggable легко комбинируется с другими компонентами React, включая формы, кнопки и сложные карточки. Важно:

  • Разделять логическую часть (состояние) и визуальную часть (DOM).
  • Для сложных карточек использовать ref на обертку, чтобы Draggable не пытался перетаскивать внутренние элементы формы.
  • Следить, чтобы события drag не блокировали другие события (click, input) внутри карточки.
<div ref={ref} className="draggable-card">
  <input type="text" value={value} onCha nge={onChange} />
</div>

Резюме технических особенностей

  • Использовать ref для контейнера и useEffect для инициализации Draggable.
  • Обновлять состояние React, а не напрямую DOM, при событиях drag.
  • Применять Sortable для списков, Draggable для низкоуровневых операций.
  • Использовать key для элементов списка, чтобы React корректно отслеживал позиции.
  • Управлять несколькими контейнерами через массив refs.
  • Настраивать анимацию через CSS или mirror API.
  • Минимизировать пересоздание Draggable и использовать мемоизацию callback-функций.

Такой подход обеспечивает полное соответствие React и Shopify Draggable, позволяя создавать высоко интерактивные интерфейсы без конфликтов между виртуальным DOM и прямыми манипуляциями с DOM.