React интеграция

Для использования Interact.js в React-проекте сначала необходимо установить библиотеку через npm:

npm install interactjs

После установки импортируется основной объект interact:

import interact from 'interactjs';

Важно помнить, что React управляет виртуальным DOM, поэтому любые прямые манипуляции с элементами DOM через Interact.js должны учитывать жизненный цикл компонентов. Наиболее корректным способом инициализации взаимодействий является использование хуков useEffect для функциональных компонентов или методов жизненного цикла в классовых компонентах.


Инициализация draggable и resizable компонентов

Для создания перетаскиваемого элемента используется метод interact с цепочкой вызовов:

import { useEffect, useRef } from 'react';
import interact from 'interactjs';

function DraggableBox() {
  const boxRef = useRef(null);

  useEffect(() => {
    interact(boxRef.current)
      .draggable({
        listeners: {
          start(event) {
            console.log('Drag start', event);
          },
          move(event) {
            const target = event.target;
            const x = (parseFloat(target.getAttribute('data-x')) || 0) + event.dx;
            const y = (parseFloat(target.getAttribute('data-y')) || 0) + event.dy;

            target.style.transform = `translate(${x}px, ${y}px)`;
            target.setAttribute('data-x', x);
            target.setAttribute('data-y', y);
          },
          end(event) {
            console.log('Drag end', event);
          }
        }
      });
  }, []);

  return <div ref={boxRef} style={{ width: 100, height: 100, background: 'steelblue' }} />;
}

Ключевые моменты:

  • Использование useRef позволяет получить доступ к реальному DOM-элементу без конфликта с виртуальным DOM.
  • Атрибуты data-x и data-y служат для хранения текущей позиции элемента.
  • События start, move, end позволяют отслеживать жизненный цикл перетаскивания.

Для изменения размера элемента добавляется метод resizable:

interact(boxRef.current)
  .resizable({
    edges: { left: true, right: true, bottom: true, top: true },
    listeners: {
      move(event) {
        let { x, y } = event.target.dataset;
        x = parseFloat(x) || 0;
        y = parseFloat(y) || 0;

        Object.assign(event.target.style, {
          width: `${event.rect.width}px`,
          height: `${event.rect.height}px`,
          transform: `translate(${x + event.deltaRect.left}px, ${y + event.deltaRect.top}px)`
        });

        event.target.dataset.x = x + event.deltaRect.left;
        event.target.dataset.y = y + event.deltaRect.top;
      }
    },
    modifiers: [
      interact.modifiers.restrictEdges({
        outer: 'parent'
      })
    ],
    inertia: true
  });

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

  • edges определяют, с каких сторон можно изменять размер.
  • modifiers.restrictEdges ограничивает размеры и положение элементом-родителем.
  • inertia: true добавляет эффект инерции при отпускании мыши.

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

Хотя Interact.js напрямую управляет стилями DOM, иногда необходимо синхронизировать данные с состоянием React для реактивного интерфейса:

import { useState, useEffect, useRef } from 'react';
import interact from 'interactjs';

function ControlledBox() {
  const boxRef = useRef(null);
  const [position, setPosition] = useState({ x: 0, y: 0 });

  useEffect(() => {
    interact(boxRef.current)
      .draggable({
        listeners: {
          move(event) {
            const x = position.x + event.dx;
            const y = position.y + event.dy;

            boxRef.current.style.transform = `translate(${x}px, ${y}px)`;
            setPosition({ x, y });
          }
        }
      });
  }, [position]);

  return <div ref={boxRef} style={{ width: 120, height: 120, background: 'tomato' }} />;
}

Важные моменты:

  • Использование состояния позволяет компоненту быть контролируемым и взаимодействовать с другими React-элементами.
  • Важно избегать конфликтов между обновлением стиля через Interact.js и React-рендером. Обычно лучше хранить координаты в state и обновлять DOM напрямую только через Interact.js во время drag/resizing.

Сложные взаимодействия: snap, restrict и inertia

Interact.js позволяет создавать продвинутые пользовательские сценарии:

interact(boxRef.current)
  .draggable({
    modifiers: [
      interact.modifiers.snap({
        targets: [interact.createSnapGrid({ x: 50, y: 50 })],
        range: Infinity,
        relativePoints: [{ x: 0, y: 0 }]
      }),
      interact.modifiers.restrictRect({
        restriction: 'parent',
        endOnly: true
      })
    ],
    inertia: true
  });
  • Snap: привязка элемента к сетке или точкам.
  • RestrictRect: ограничение движения в пределах контейнера.
  • Inertia: добавляет плавность движению, имитируя физику.

Использование Interact.js с динамическими элементами

Для динамически создаваемых компонентов важно инициализировать Interact.js после их рендера. Часто используется useEffect с зависимостями, отслеживающими массив элементов:

useEffect(() => {
  elements.forEach(ref => {
    interact(ref.current).draggable({ /* конфигурация */ });
  });
}, [elements]);

Это позволяет создавать интерфейсы с любым количеством draggable или resizable блоков без нарушения реактивности.


События multi-touch и gestures

Interact.js поддерживает жесты pinch, rotate и multi-touch. Их интеграция в React выглядит следующим образом:

interact(boxRef.current)
  .gesturable({
    listeners: {
      move(event) {
        const scale = (parseFloat(event.target.dataset.scale) || 1) * (1 + event.ds);
        event.target.style.transform = `scale(${scale})`;
        event.target.dataset.scale = scale;
      }
    }
  });

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

  • gesturable позволяет обрабатывать pinch, rotate и другие мультитач-события.
  • event.ds — изменение масштаба за текущее событие.
  • Использование dataset удобно для хранения текущих параметров трансформации.

Поддержка нескольких взаимодействий одновременно

Interact.js позволяет комбинировать draggable, resizable и gesturable на одном элементе:

interact(boxRef.current)
  .draggable({ inertia: true })
  .resizable({ edges: { left: true, right: true, bottom: true, top: true } })
  .gesturable();

Важно правильно обрабатывать transform, чтобы сочетание масштабирования, вращения и перемещения не конфликтовало. Обычно хранение состояния трансформации в dataset с раздельными полями x, y, scale, angle обеспечивает корректное комбинирование действий.


Практические советы

  • Для производительности использовать requestAnimationFrame внутри обработчиков move для сложных интерфейсов.
  • Всегда очищать взаимодействия при размонтировании компонента: interact(boxRef.current).unset().
  • Использовать modifiers для ограничения движения и размеров вместо контроля через React-состояние.
  • Для массивов динамических элементов создать кастомный хук useInteract для унификации конфигурации.