使用useEffect钩子将addEventListener限制为componentDidMount

时间:2020-06-25 09:44:48

标签: reactjs react-hooks react-functional-component react-class-based-component

我有一个基于类的组件,该组件使用多点触控将子节点添加到svg中,并且效果很好。现在,我尝试将其更新为使用带有钩子的功能组件,除非是为了更好地理解钩子。

为了停止使用触摸事件进行手势的浏览器,我需要对它们preventDefault进行svgRef.current.addEventListener('touchstart', handler, {passive: false})操作,这要求它们passive,并且由于缺乏我需要使用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> ); } 挂钩中将其清除。

当我将其翻译为带有钩子的功能组件时,我得到以下结果:

useEffect

引起的问题是,每次进行渲染更新时,事件监听器都将被删除并重新添加。这将导致在有机会清除其他奇数异象之前删除handleTouchEnd。我还发现,除非使用手势退出浏览器并触发更新,删除现有的侦听器并添加新的设置,否则触摸事件将无法正常工作。

我尝试在useEffect中使用依赖项列表,并且看到一些人引用useCallback和useRef,但是我无法使其更好地工作(例如,删除然后重新添加的日志)事件监听器仍然会在每次更新时触发)。

是否有一种方法可以使useEffect在安装时仅触发一次,然后在卸载时进行清理,还是我应该放弃该组件的钩子并坚持使用运行良好的基于​​类的类?

编辑

我还尝试将每个事件侦听器移动到自己的remove touch start remove touch move remove touch end add touch start add touch move add touch end 中,并获取以下控制台日志:

    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]);

编辑2

几个人建议添加一个我尝试过的依赖项数组:

useEffect

但是我仍然收到一条日志,说每个touchstart已被删除,然后在每次更新时重新添加(因此,每个touchmovetouchend或{ {1}}会产生油漆-很多:))

编辑3

我已将window.(add/remove)EventListener替换为useRef()

ta

2 个答案:

答案 0 :(得分:2)

如果只希望在安装和卸载组件时发生这种情况,则需要为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 });
    }
}, []);

答案 1 :(得分:1)

非常感谢大家-我们深入了解(w00t)

为了使组件useEffect挂钩多次触发,需要向挂钩提供一个空的依赖项数组(如Son Nguyenwentjun所建议),但这意味着当前的touchPoints状态无法在处理程序中访问。

答案(由godjun建议)在How to fix missing dependency warning when using 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添加到依赖项列表中是为了更具声明性

蒙多尊敬的人

;oB