Создавая клон моей любимой в детстве игры, мне внезапно понадобились перетаскиваемые панели управления. Требование к ним было довольно простым: когда вы щелкаете по их заголовку и удерживаете, панель должна перетаскиваться, кося мышью.

Я уверен, что существует множество инструментов, которые вы можете просто добавить в свой проект, но я хотел сначала сделать это самостоятельно. Даю себе возможность больше узнать о React, программировании с графическим интерфейсом и написать больше кода.

Я начал с создания нового проекта с помощью create-react-app и написал простейшую разметку для панели инструментов:

Мне нравится, чтобы мой код всегда был максимально простым. Итак, давайте просто начнем делать инструмент перетаскиваемым сам по себе. Сначала мне нужно какое-то состояние: верхнее и левое значения, чтобы разместить панель, и логическое значение, сообщающее мне, нажата ли мышь или нет. Я решил, что логическое значение должно переключаться в значение true только при нажатии на сам заголовок, но снова должно устанавливаться в значение false при отпускании кнопки мыши, независимо от того, где находится курсор (чтобы избежать ошибок, когда курсор мог покинуть заголовок до отпускания ).

Мне также нужно знать, где находится курсор мыши (и обновить верхнюю и левую часть), и когда снова отпустить кнопку мыши. Однако мне нужно знать это только тогда, когда кнопка мыши нажата. И я знаю идеальное решение для этого. useEffect с зависимостью mouseDown, поэтому я могу добавлять прослушиватели событий при нажатии кнопки мыши и снова удалять их при отпускании мыши.

Обработчик события mouseDown отправляется дальше вниз к компоненту Title. (Как пример сейчас, мне действительно не нужно было бы создавать компоненты. В реальном сценарии он, вероятно, содержал бы больше функций, чтобы оправдать то, что он является компонентом.)

Это работает почти идеально, но: При перетаскивании панели ее верхний левый угол перемещается туда, где находится курсор мыши. Чтобы избежать этого, я просто создал дельту, чтобы убедиться, что панель перемещается относительно того места, где находился курсор при запуске перетаскивания. Я назвал переменные startTop и startLeft и обновил их состояние по mouseDown.

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

Использование useRef, чтобы избежать ненужных рендеров

Приведенный выше код заставит eslint кричать на вас (при условии, что вы используете плагин eslint-plugin-react-hooks). В частности, мы ссылаемся на startTop и startLeft в пределах useEffect, не имея в своем массиве зависимостей. В то же время: я хочу, чтобы этот useEffect влиял только тогда, когда mouseDown переворачивает состояние: включение обработчиков событий, когда кнопка мыши удерживается нажатой, и их отключение при отпускании кнопки мыши.

Я исправил это, поместив вместо этого startTop и startLeft в ref. Это идеально подходит для нашего варианта использования, потому что startTop и startLeft в любом случае не являются состоянием хука. Ничто не должно перерисовываться, потому что мы определяем значения startTop или startLeft. На самом деле, если бы существовал правильный способ определения местоположения курсора мыши без использования события, мы могли бы даже просто определить startLeft и startTop как константные значения внутри useEffect-лямбда!

Заключительные замечания

Как видите, мой useDraggable также использует дроссель lodash для обработчика событий mouseMove. На самом деле это не является строго необходимым, а также вы можете захотеть использовать функцию дросселя из какой-нибудь другой библиотеки, кроме lodash — скажем, если у вас уже есть дроссель где-то в вашем коде или deps.

Конечно, есть также много способов сделать этот хук более сложным:

  • Добавьте параметры, где начинать сверху и начинать слева, вместо того, чтобы жестко запрограммировать его на 20 пикселей.
  • Добавьте параметры того, как далеко вниз или вправо вы должны перетаскивать компонент.
  • Содержите перетаскиваемый компонент в контейнере, а не в самом окне просмотра (что в моем примере молчаливо предполагается)

Однако последний пункт может быть немного сложным: вы бы хотели использовать координаты e.pageX/e.pageY для указателя мыши, потому что координаты «контейнера» для указателя мыши глючат (особенно в Firefox, где иногда ошибочно 0).

Полностью рабочий пример (без дросселирования onMove) можно найти в песочнице кода:

https://codesandbox.io/embed/usedraggable-hook-example-x82n4?fontsize=14&hidenavigation=1&theme=dark