Ограничение addEventListener до componentDidMount с помощью хука useEffect
У меня есть компонент на основе классов, который использует мультитач для добавления дочерних узлов в SVG, и это хорошо работает. Теперь я пытаюсь обновить его, чтобы использовать функциональный компонент с хуками, хотя бы по той причине, что лучше их понять.
Чтобы браузер не использовал сенсорные события для жестов, мне нужно preventDefault
на них, что требует, чтобы они не были пассивными, и из-за отсутствия раскрытия пассивной конфигурации в синтетических событиях реакции мне нужно было использоватьsvgRef.current.addEventListener('touchstart', handler, {passive: false})
. Я делаю это вcomponentDidMount()
жизненного цикла и очистите его в componentWillUnmount()
крючок внутри класса.
Когда я перевожу это на функциональный компонент с хуками, я получаю следующее:
export default function Board(props) {
const [touchPoints, setTouchPoints] = useState([]);
const svg = useRef();
useEffect(() => {
console.log('add touch start');
svg.current.addEventListener('touchstart', handleTouchStart, { passive: false });
return () => {
console.log('remove touch start');
svg.current.removeEventListener('touchstart', handleTouchStart, { passive: false });
}
});
useEffect(() => {
console.log('add touch move');
svg.current.addEventListener('touchmove', handleTouchMove, { passive: false });
return () => {
console.log('remove touch move');
svg.current.removeEventListener('touchmove', handleTouchMove, { passive: false });
}
});
useEffect(() => {
console.log('add touch end');
svg.current.addEventListener('touchcancel', handleTouchEnd, { passive: false });
svg.current.addEventListener('touchend', handleTouchEnd, { passive: false });
return () => {
console.log('remove touch end');
svg.current.removeEventListener('touchend', handleTouchEnd, { passive: false });
svg.current.removeEventListener('touchcancel', handleTouchEnd, { passive: false });
}
});
const handleTouchStart = useCallback((e) => {
e.preventDefault();
// copy the state, mutate it, re-apply it
const tp = touchPoints.slice();
// note e.changedTouches is a TouchList not an array
// so we can't map over it
for (var i = 0; i < e.changedTouches.length; i++) {
const touch = e.changedTouches[i];
tp.push(touch);
}
setTouchPoints(tp);
}, [touchPoints, setTouchPoints]);
const handleTouchMove = useCallback((e) => {
e.preventDefault();
const tp = touchPoints.slice();
for (var i = 0; i < e.changedTouches.length; i++) {
const touch = e.changedTouches[i];
// call helper function to get the Id of the touch
const index = getTouchIndexById(tp, touch);
if (index < 0) continue;
tp[index] = touch;
}
setTouchPoints(tp);
}, [touchPoints, setTouchPoints]);
const handleTouchEnd = useCallback((e) => {
e.preventDefault();
const tp = touchPoints.slice();
for (var i = 0; i < e.changedTouches.length; i++) {
const touch = e.changedTouches[i];
const index = getTouchIndexById(tp, touch);
tp.splice(index, 1);
}
setTouchPoints(tp);
}, [touchPoints, setTouchPoints]);
return (
<svg
xmlns={ vars.SVG_NS }
width={ window.innerWidth }
height={ window.innerHeight }
>
{
touchPoints.map(touchpoint =>
<TouchCircle
ref={ svg }
key={ touchpoint.identifier }
cx={ touchpoint.pageX }
cy={ touchpoint.pageY }
colour={ generateColour() }
/>
)
}
</svg>
);
}
Проблема, возникающая при этом, заключается в том, что каждый раз при обновлении рендеринга все прослушиватели событий удаляются и добавляются повторно. Это приводит к тому, что handleTouchEnd удаляется, прежде чем у него появится возможность убрать добавленные штрихи среди других странностей. Я также обнаружил, что события касания не работают, если я не использую жест для выхода из браузера, который запускает обновление, удаляя существующие прослушиватели и добавляя новый набор.
Я попытался использовать список зависимостей в useEffect, и я видел, как несколько человек ссылались на useCallback и useRef, но я не смог улучшить эту работу (т.е. журналы для удаления, а затем повторного добавления прослушивателей событий все еще все срабатывают при каждом обновлении).
Есть ли способ заставить useEffect срабатывать только один раз при монтировании, а затем очищать при размонтировании, или мне следует отказаться от хуков для этого компонента и придерживаться класса, который работает хорошо?
редактировать
Я также попытался переместить каждый прослушиватель событий в свой собственный useEffect
и получите следующие журналы консоли:
remove touch start
remove touch move
remove touch end
add touch start
add touch move
add touch end
Редактировать 2
Несколько человек предложили добавить массив зависимостей, который я пробовал вот так:
useEffect(() => {
console.log('add touch start');
svg.current.addEventListener('touchstart', handleTouchStart, { passive: false });
return () => {
console.log('remove touch start');
svg.current.removeEventListener('touchstart', handleTouchStart, { passive: false });
}
}, [handleTouchStart]);
useEffect(() => {
console.log('add touch move');
svg.current.addEventListener('touchmove', handleTouchMove, { passive: false });
return () => {
console.log('remove touch move');
svg.current.removeEventListener('touchmove', handleTouchMove, { passive: false });
}
}, [handleTouchMove]);
useEffect(() => {
console.log('add touch end');
svg.current.addEventListener('touchcancel', handleTouchEnd, { passive: false });
svg.current.addEventListener('touchend', handleTouchEnd, { passive: false });
return () => {
console.log('remove touch end');
svg.current.removeEventListener('touchend', handleTouchEnd, { passive: false });
svg.current.removeEventListener('touchcancel', handleTouchEnd, { passive: false });
}
}, [handleTouchEnd]);
но я все еще получаю журнал, в котором говорится, что каждый из useEffect
были удалены, а затем снова добавлены при каждом обновлении (так что каждый touchstart
, touchmove
или touchend
которая наносит краску - а это много:))
Редактировать 3
Я заменил window.(add/remove)EventListener
с useRef()
та
2 ответа
Большое спасибо, ребята - мы дошли до сути (w00t)
Чтобы остановить компонент useEffect
при многократном срабатывании крючка требуется предоставить ему пустой массив зависимостей (как было предложено Son Nguyen и wentjun), однако это означает, чтоtouchPoints
состояние не было доступно в обработчиках.
Ответ (предложенный gojun) был в том, как исправить предупреждение об отсутствии зависимости при использовании useEffect React Hook?
в котором упоминаются часто задаваемые вопросы о хуках: https://reactjs.org/docs/hooks-faq.html#what-can-i-do-if-my-effect-dependencies-change-too-often
вот как закончился мой компонент
export default function Board(props) {
const [touchPoints, setTouchPoints] = useState([]);
const svg = useRef();
useEffect(() => {
// required for the return value
const svgRef = svg.current;
const handleTouchStart = (e) => {
e.preventDefault();
// use functional version of mutator
setTouchPoints(tp => {
// duplicate array
tp = tp.slice();
// note e.changedTouches is a TouchList not an array
// so we can't map over it
for (var i = 0; i < e.changedTouches.length; i++) {
const touch = e.changedTouches[i];
const angle = getAngleFromCenter(touch.pageX, touch.pageY);
tp.push({ touch, angle });
}
return tp;
});
};
const handleTouchMove = (e) => {
e.preventDefault();
setTouchPoints(tp => {
tp = tp.slice();
// move existing TouchCircle with same key
for (var i = 0; i < e.changedTouches.length; i++) {
const touch = e.changedTouches[i];
const index = getTouchIndexById(tp, touch);
if (index < 0) continue;
tp[index].touch = touch;
tp[index].angle = getAngleFromCenter(touch.pageX, touch.pageY);
}
return tp;
});
};
const handleTouchEnd = (e) => {
e.preventDefault();
setTouchPoints(tp => {
tp = tp.slice();
// delete existing TouchCircle with same key
for (var i = 0; i < e.changedTouches.length; i++) {
const touch = e.changedTouches[i];
const index = getTouchIndexById(tp, touch);
if (index < 0) continue;
tp.splice(index, 1);
}
return tp;
});
};
console.log('add touch listeners'); // only fires once
svgRef.addEventListener('touchstart', handleTouchStart, { passive: false });
svgRef.addEventListener('touchmove', handleTouchMove, { passive: false });
svgRef.addEventListener('touchcancel', handleTouchEnd, { passive: false });
svgRef.addEventListener('touchend', handleTouchEnd, { passive: false });
return () => {
console.log('remove touch listeners');
svgRef.removeEventListener('touchstart', handleTouchStart, { passive: false });
svgRef.removeEventListener('touchmove', handleTouchMove, { passive: false });
svgRef.removeEventListener('touchend', handleTouchEnd, { passive: false });
svgRef.removeEventListener('touchcancel', handleTouchEnd, { passive: false });
}
}, [setTouchPoints]);
return (
<svg
ref={ svg }
xmlns={ vars.SVG_NS }
width={ window.innerWidth }
height={ window.innerHeight }
>
{
touchPoints.map(touchpoint =>
<TouchCircle
key={ touchpoint.touch.identifier }
cx={ touchpoint.touch.pageX }
cy={ touchpoint.touch.pageY }
colour={ generateColour() }
/>
)
}
</svg>
);
}
Примечание: я добавил setTouchPoints
в список зависимостей, чтобы быть более декларативным
Mondo уважает парней
;oB
Если вы хотите, чтобы это происходило только тогда, когда компонент монтируется и размонтируется, вам нужно будет предоставить ловушке useEffect пустой массив в качестве массива зависимостей.
useEffect(() => {
console.log('adding event listeners');
window.addEventListener('touchstart', handleTouchStart, { passive: false });
window.addEventListener('touchend', handleTouchEnd, { passive: false });
window.addEventListener('touchcancel', handleTouchEnd, { passive: false });
window.addEventListener('touchmove', handleTouchMove, { passive: false });
return () => {
console.log('removing event listeners');
window.removeEventListener('touchstart', handleTouchStart, { passive: false });
window.removeEventListener('touchend', handleTouchEnd, { passive: false });
window.removeEventListener('touchcancel', handleTouchEnd, { passive: false });
window.removeEventListener('touchmove', handleTouchMove, { passive: false });
}
}, []);