I have a class-based component which uses multitouch to add child nodes to an svg and this works well. Now, I am trying to update it to use a functional component with hooks, if for no other reason than to better understand them.
In order to stop the browser using the touch events for gestures, I need to preventDefault on them which requires them to not be passive and, because of the lack of exposure of the passive configuration within synthetic react events I've needed to use svgRef.current.addEventListener('touchstart', handler, {passive: false}). I do this in the componentDidMount() lifecycle hook and clear it in the componentWillUnmount() hook within the class.
When I translate this to a functional component with hooks, I end up with the following:
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>
    );
}
The issue this raises is that every time there is a render update, the event listeners all get removed and re-added. This causes the handleTouchEnd to be removed before it has a chance to clear up added touches among other oddities. I'm also finding that the touch events aren't working unless i use a gesture to get out of the browser which triggers an update, removing the existing listeners and adding a fresh set.
I've attempted to use the dependency list in useEffect and I have seen several people referencing useCallback and useRef but I haven't been able to make this work any better (ie, the logs for removing and then re-adding the event listeners still all fire on every update).
Is there a way to make the useEffect only fire once on mount and then clean up on unmount or should i abandon hooks for this component and stick with the class based one which is working well?
Edit
I've also tried moving each event listener into its own useEffect and get the following console logs:
remove touch start
remove touch move
remove touch end
add touch start
add touch move
add touch end
Edit 2
A couple of people have suggested adding a dependency array which I've tried like this:
    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]);
but I'm still receiving a log to say that each of the useEffects have been removed and then re-added on each update (so every touchstart, touchmove or touchend which causes a paint - which is a lot :) )
Edit 3
I've replaced window.(add/remove)EventListener with useRef()
ta
 
    