Ограничение 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 });
    }
}, []);
Другие вопросы по тегам