Замороженное закрытие с использованием callback / useMemo - PullRequest
1 голос
/ 22 сентября 2019

Я наблюдаю странное поведение, при котором функция useCallback не может получить доступ к внешней области видимости.

Это описано здесь: Как React Hooks использует CallCallback "зависает"замыкание?

Хотя его можно легко пересчитать, добавляя все элементы в массив, оно кажется нелогичным.Поэтому мне интересно, как лучше с этим справиться.

Вот код

import React, { useCallback, useState, useEffect, useRef } from 'react';
import classNames from 'classnames';
import useEventListener from '../../hooks/use-event-listener';
import tinybounce from 'tinybounce';
import Scroll from '../../utils/scroll';

export default function DefaultMainPlayer({ children, className }) {
    const [scrollThreshold, setScrollThreshold] = useState(0);
    const [inView, setInView] = useState(true);
    const [didUserClose, setDidUserClose] = useState(false);
    const el = useRef();
    const scrollHandler = useCallback(() => {
        // scrollThreshold and inView get "frozen" unless they're on the array below
        const isVisible = Scroll.getPosition() <= scrollThreshold;

        if (inView !== isVisible) {
            if (inView) {
                setInView(isVisible);
                setDidUserClose(false);
            }
        }
    }, [scrollThreshold, inView]);
    const cx = classNames('main-player', className, {
        'is-fixed': !inView && !didUserClose
    });
    const resizeHandler = useCallback(
        tinybounce(() => setScrollThreshold(Scroll.getElementCoordinates(el.current).bottom), 300),
        []
    );

    // If scroll threshold updates, lets call the scroll handler
    useEffect(scrollHandler, [scrollThreshold]);
    // Call the resize handler once
    useEffect(resizeHandler, []);

    // I'd like the scroll handler to never change since it really doesn't need to
    useEventListener('scroll', scrollHandler);
    useEventListener('resize', resizeHandler);

    return (
        <div className={cx} ref={el}>{children}</div>
    );
}

Вот код для `use-event-listener``

import { useRef, useEffect } from 'react';

export default function useEventListener(eventName, handler, element = window) {
    const savedHandler = useRef();

    useEffect(() => {
        savedHandler.current = handler;
    }, [handler]);

    useEffect(() => {
        const isSupported = element && element.addEventListener;
        if (!isSupported) return;

        const eventListener = event => savedHandler.current(event);

        // Add event listener
        element.addEventListener(eventName, eventListener);

        // Remove event listener on cleanup
        return () => {
            element.removeEventListener(eventName, eventListener);
        };
    },
    [eventName, element]
    );
};

Я стараюсь не менять scrollHandler все время, потому что функции не равны.Я пытался useMemo (возвращая функцию в качестве значения), но результат тот же.Не обновляйте scrollThreshold.

Хотя эти две переменные могут показаться «маленькими», им может понадобиться больше, и это просто кажется неправильным.

Есть ли способ исправить это илиподходить к этому по-другому?

1 Ответ

1 голос
/ 22 сентября 2019

Кажется, проблема в том, что вы используете переменную состояния внутри обратного вызова, которая затем устанавливается внутри useEffect.Вероятно, не существует простого способа справиться с этим, см. здесь .

Два варианта


Не использовать useCallback.Таким образом, при запуске функции будут использоваться самые последние scrollThreshold и inView.Также передайте изменяемые переменные в useEventListener или любой хук, который его использует, как зависимости useEffect (для этого потребуется некоторый рефакторинг):

useEffect(() => {
    const isSupported = element && element.addEventListener;
    if (!isSupported) return;

    const eventListener = handler;

    // Add event listener
    element.addEventListener(eventName, eventListener);

    // Remove event listener on cleanup
    return () => {
        element.removeEventListener(eventName, eventListener);
    };
}, [eventName, element, scrollThreshold, inView]);

Используйте useCallback с scrollThreshold, inView как зависимости.В useEventListener или любом хуке, который его использует, используйте функцию, переданную в качестве зависимости в обработчике добавления / удаления событий:

useEffect(() => {
    const isSupported = element && element.addEventListener;
    if (!isSupported) return;

    const eventListener = handler;

    // Add event listener
    element.addEventListener(eventName, eventListener);

    // Remove event listener on cleanup
    return () => {
        element.removeEventListener(eventName, eventListener);
    };
}, [eventName, element, handler]);

Дайте мне знать, если что-то не понятно.

...