Добавление и удаление прослушивателя перемещения мыши в окне с помощью реактивных крючков

#reactjs #react-hooks #addeventlistener #mousemove #removeeventlistener

#reactjs #реагирующие перехваты #addeventlistener #mousemove #removeeventlistener

Вопрос:

Я пытаюсь добавить прослушиватель событий window при щелчке по объекту, а затем удалить этот прослушиватель событий при повторном щелчке по объекту.

При Card нажатии на компонент состояние isCardMoving включается или выключается.

Я добавил a useEffect для просмотра isCardMoving . Когда isCardMoving он включен, он должен добавить прослушиватель mousemove событий в окно, которое запускает handleCardMove функцию. Эта функция просто регистрирует координаты мыши.

если я снова нажму на карточку, isCardMoving будет false, и я ожидаю, что прослушиватель событий в окне будет удален внутри useEffect .

Однако происходит то, что прослушиватель событий будет добавлен при isCardMoving true появлении, а затем не будет удален после isCardMoving false появления .

 import React from 'react';

const App = () => {
  const [isCardMoving, setIsCardMoving] = React.useState(false);

  React.useEffect(() => {
    if (isCardMoving) window.addEventListener('mousemove', handleCardMove);
    else window.removeEventListener('mousemove', handleCardMove);
  }, [isCardMoving]);

  const handleCardMove = (event) => console.log({ x: event.offsetX, y: event.offsetY });

  return <Card onClick={() => setIsCardMoving(!isCardMoving)} />;
};
  

Затем я попытался установить a ref в окне, думая, что, возможно, по какой-то причине мне понадобится предыдущая ссылка на окно:

 import React from 'react';

const App = () => {
  const [isCardMoving, setIsCardMoving] = React.useState(false);

  const windowRef = React.useRef(window); // add window ref

  // update window ref whenever window is updated
  React.useEffect(() => {
    windowRef.current = window;
  }, [window]);

  React.useEffect(() => {
    // add and remove event listeners on windowRef
    if (isCardMoving) windowRef.current.addEventListener('mousemove', handleCardMove);
    else windowRef.current.removeEventListener('mousemove', handleCardMove);
  }, [isCardMoving]);

  const handleCardMove = (event) => console.log({ x: event.offsetX, y: event.offsetY });

  return <Card onClick={() => setIsCardMoving(!isCardMoving)} />;
};
  

Похоже, это имеет тот же эффект, что и раньше.

Ответ №1:

На самом деле, вы не можете удалить прослушиватель событий, подобный этому, в React или любых других виртуальных приложениях на основе DOM. Из-за природы библиотек виртуальных DOMs вам необходимо удалить прослушиватель событий в жизненном цикле размонтирования, который находится в перехватах реакции, он доступен внутри useEffect самого. Итак, вы должны сделать это, как показано ниже, с ключевым словом return, оно будет делать то же самое, componentWillUnmount что и в базовых компонентах класса:

 React.useEffect(() => {
    if (isCardMoving) window.addEventListener("mousemove", handleCardMove);
    return () => window.removeEventListener("mousemove", handleCardMove);
}, [isCardMoving]);
  

Рабочая демонстрация:

CodeSandbox

Обновить

Как сказал @ZacharyHaber в комментариях, основная причина такого поведения заключается в том, что ваша handleCardMove функция будет переопределяться при каждом рендеринге, поэтому, чтобы преодолеть эту ситуацию, нам нужно отвязать событие от окна при каждом рендеринге с useEffect помощью обратного вызова. Вы также можете заставить свой исходный код работать с использованием useCallback подхода using, но вам также необходимо добавить предыдущий useEffect обратный вызов к вашему компоненту, чтобы убедиться, что прослушиватель событий будет удален в цикле размонтирования компонента, это немного больше кода, но этот будет делать то же самое, что и описанный выше подход.

 const handleCardMove = React.useCallback((event) => {
   console.log({ x: event.offsetX, y: event.offsetY });
}, []);

React.useEffect(() => {
  if (isCardMoving) window.addEventListener("mousemove", handleCardMove);
  else window.removeEventListener("mousemove", handleCardMove);
  return () => window.removeEventListener("mousemove", handleCardMove);
}, [isCardMoving, handleCardMove]);
  

Рабочая демонстрация:

CodeSandbox

Комментарии:

1. Речь идет не столько о том, как работают виртуальные библиотеки dom, сколько о том, что handleCardMove переопределяется при каждом рендеринге. Для удаления прослушивателей событий требуется дескриптор функции, которая была добавлена изначально. Например, использование useCallback((e)=>console.log(e),[]) сделало бы так, чтобы исходный код работал

2. @SMAKSS @Zachary Haber спасибо, это имеет смысл. Я должен помнить this…it вызывает ли возвращенный обратный вызов, когда isCardMoving обновляется во второй раз (и возвращается в положение выкл)? Нужно будет найти некоторые ресурсы по этому поводу

3. @rpivovar Да, Захарий действительно прав. Если вы поместите свою функцию внутри a useCallBack и добавите ее в массив useEffect зависимостей, вы можете использовать ее без return и с первоначальным подходом, который вы пробовали.

4. Хотя, не делайте этого, потому что тогда прослушиватель событий не будет очищен при отключении компонента, так что это не идеальный способ сделать это. Поэтому я просто прокомментировал useCallback метод в качестве примечания, а не разместил отдельный ответ. Этот ответ — правильный способ сделать это, просто с небольшим изменением идей, стоящих за ним 🙂

5. Вау — просто попробовал обернуть handleCardMove в a useCallback . Я буду придерживаться возврата в useEffect… но спасибо за понимание вам обоим. Действительно помогает мне улучшить мое понимание обоих этих перехватов.