#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]);
Рабочая демонстрация:
Обновить
Как сказал @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]);
Рабочая демонстрация:
Комментарии:
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
в auseCallback
. Я буду придерживаться возврата в useEffect… но спасибо за понимание вам обоим. Действительно помогает мне улучшить мое понимание обоих этих перехватов.