使用 useEffect 挂钩将 addEventListener 限制为 componentDidMount
Limiting addEventListener to componentDidMount using useEffect hook
我有一个基于 class 的组件,它使用多点触控将子节点添加到 svg,并且效果很好。现在,我正在尝试更新它以使用带钩子的功能组件,如果没有其他原因,只是为了更好地理解它们。
为了停止浏览器对手势使用触摸事件,我需要对它们进行 preventDefault
,这要求它们 而不是 passive 并且,因为在我需要使用 svgRef.current.addEventListener('touchstart', handler, {passive: false})
的合成反应事件中没有暴露被动配置。我在 componentDidMount()
生命周期挂钩中执行此操作,并在 class.
中的 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 只在挂载时触发一次,然后在卸载时清理,或者我应该放弃这个组件的挂钩并坚持使用基于 class 的运行良好的挂钩?
编辑
我也试过将每个事件侦听器移动到它自己的 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()
ta
如果您只希望在安装和卸载组件时发生这种情况,则需要为 useEffect 挂钩提供 empty array as the dependency array.
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 });
}
}, []);
非常感谢大家 - 我们已经弄清楚了 (w00t)
为了停止多次触发组件 useEffect
挂钩,需要向挂钩提供一个空的依赖数组(如 Son Nguyen and wentjun 所建议),但这意味着当前 touchPoints
处理程序中无法访问状态。
答案(wentjun 建议)在
其中提到了钩子常见问题解答: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
我有一个基于 class 的组件,它使用多点触控将子节点添加到 svg,并且效果很好。现在,我正在尝试更新它以使用带钩子的功能组件,如果没有其他原因,只是为了更好地理解它们。
为了停止浏览器对手势使用触摸事件,我需要对它们进行 preventDefault
,这要求它们 而不是 passive 并且,因为在我需要使用 svgRef.current.addEventListener('touchstart', handler, {passive: false})
的合成反应事件中没有暴露被动配置。我在 componentDidMount()
生命周期挂钩中执行此操作,并在 class.
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 只在挂载时触发一次,然后在卸载时清理,或者我应该放弃这个组件的挂钩并坚持使用基于 class 的运行良好的挂钩?
编辑
我也试过将每个事件侦听器移动到它自己的 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()
ta
如果您只希望在安装和卸载组件时发生这种情况,则需要为 useEffect 挂钩提供 empty array as the dependency array.
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 });
}
}, []);
非常感谢大家 - 我们已经弄清楚了 (w00t)
为了停止多次触发组件 useEffect
挂钩,需要向挂钩提供一个空的依赖数组(如 Son Nguyen and wentjun 所建议),但这意味着当前 touchPoints
处理程序中无法访问状态。
答案(wentjun 建议)在
其中提到了钩子常见问题解答: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