Я пытаюсь добавить и удалить прослушиватели событий вдовы при открытии и закрытии компонента.
Проблема в том, что isOpen в обработчике всегда находится в исходном состоянии и не использует последнее состояние. Как я могу решить эту проблему? Я видел много решений, использующих useEffect(() => { .. }, []);, но это не сработает, поскольку я хочу иметь возможность добавлять и удалять прослушиватели в разное время, а не при монтировании и размонтировании.
Я также пробовал использовать useCallback, но возникла та же проблема, что и useEffect.
Вот мой текущий код:
import { forwardRef, Ref, useEffect, useImperativeHandle, useRef } from 'react';
import { useState } from 'react';
export const Example = (props) => {
const userMenuRef = useRef(null);
return (
<>
<button onClick = {() => userMenuRef.current?.open()}>Open Popover</button>
<Popover ref = {userMenuRef} />
</>
);
}
type Props = {
};
export const Popover = forwardRef((props: Props, ref: Ref<any>) => {
const [isOpen, setIsOpen] = useState(false);
const [clickOutsideHandler] = useState(() =>
(evt: MouseEvent): void => {
console.info('!isOpen: ' + isOpen); // <-- issue is that `isOpen` is always false
if (!isOpen || popoverRef.current.contains(evt.target as Node)) { return; } // Clicked on one of the own children
close();
}
);
const popoverRef = useRef(null);
const open = (): void => {
if (isOpen) { return; }
setIsOpen(true);
}
const close = (): void => {
if (!isOpen) { return; }
setIsOpen(false);
}
useImperativeHandle(ref, () => ({
open,
}));
useEffect(() => {
if (isOpen) {
window.requestAnimationFrame(() => { // Wait 1 tick; otherwise a potential click that opend the element will immediately trigger a click-outside event
document.addEventListener('click', clickOutsideHandler);
});
}
else {
document.removeEventListener('click', clickOutsideHandler);
}
}, [isOpen]);
return (
<>
{(isOpen) &&
<div ref = {popoverRef}>
<div onClick = {() => { close(); }}>Some Content here</div>
</div>
}
</>
);
});
Вопрос в следующем: как я могу заставить обработчик сохраняться, сохраняя при этом доступ к правильному состоянию внутри него?





Вы определяете функцию clickOutsideHandler как useState, вам следует просто сделать ее реальной функцией, поскольку она не использует никакой логики состояния.
Я бы изменил это на что-то вроде этого (без ts)
const clickOutsideHandler = (evt) => {
if (!isOpen || popoverRef.current.contains(evt.target)) { return; } // Clicked on one of the own children
close();
};
Тем не менее, вместо создания функций open и close вы можете использовать функцию стрелки в обработчике setState, чтобы получить текущее значение, а затем переключить его, например:
const toggleOpenState = () => setIsOpen(cur => !cur);
Если вы определите это так, document.removeEventListener('click', clickOutsideHandler); не будет работать, поскольку clickOutsideHandler будет другим экземпляром функции при повторной визуализации компонента.
Это должно работать нормально, не забудьте добавить минимальный воспроизводимый пример к исходному вопросу?
Я добавил компонент «Пример», который открывает всплывающее окно при нажатии кнопки. Если я использую ваш пример, clickOutsideHandler работает нормально в первый раз, но он не будет удален с помощью removeEventListener, поскольку он становится экземпляром функции, отличным от того, который использовался с addEventListener при повторной визуализации компонента. поэтому, когда вы нажимаете кнопку во второй раз, она открывается нормально, но также сразу же закрывается, поскольку старый прослушиватель все еще там.
Это проблема Javascript Закрытие.
Вы определяете функцию, которая закрывается по значению состояния isOpen из начального цикла рендеринга при объявлении состояния clickOutsideHandler.
const [isOpen, setIsOpen] = useState(false);
const [clickOutsideHandler] = useState(() =>
(evt: MouseEvent): void => {
console.info('!isOpen: ' + isOpen);
if (!isOpen || popoverRef.current.contains(evt.target as Node)) {
return;
} // Clicked on one of the own children
close();
}
);
Состояние clickOutsideHandler никогда не обновляется для повторного включения текущего значения состояния isOpen, поэтому оно всегда имеет начальное значение состояния false isOpen.
Вместо использования состояния React для хранения обратного вызова чаще используется хук useCallback (т. е. useState + useEffect для обновления обратного вызова -> useCallback) с соответствующими зависимостями для правильного закрытия значений и предоставления стабильной ссылки на обратный вызов.
Пример:
Обратите внимание, что clickOutsideHandler также необходимо включить в зависимости useEffect-хука, чтобы эффект был правильно настроен.
const [isOpen, setIsOpen] = useState(false);
const clickOutsideHandler = useCallback(
(evt: MouseEvent): void => {
if (!isOpen || popoverRef.current.contains(evt.target as Node)) {
return;
} // Clicked on one of the own children
close();
},
[isOpen]
);
useEffect(() => {
if (isOpen) {
window.requestAnimationFrame(() => {
document.addEventListener('click', clickOutsideHandler);
});
} else {
document.removeEventListener('click', clickOutsideHandler);
}
}, [clickOutsideHandler, isOpen]);
Приведенное выше решение не удаляет ранее добавленные прослушиватели событий. Эффект должен возвращать функцию очистки для правильного удаления ранее добавленных прослушивателей событий. Переместите объявление clickOutsideHandler в обратный вызов useEffect, чтобы оно стало внутренней ссылкой для функции очистки.
Добавляйте прослушиватель событий только тогда, когда isOpen правдиво, и удаляйте прослушиватель только в функции очистки.
Вы также можете удалить проверку isOpenclickOutsideHandler, поскольку ее можно вызвать только в том случае, если isOpen было истинно и был добавлен прослушиватель событий.
const [isOpen, setIsOpen] = useState(false);
useEffect(() => {
const clickOutsideHandler = (evt: MouseEvent): void => {
if (popoverRef.current.contains(evt.target as Node)) {
return;
} // Clicked on one of the own children
close();
};
if (isOpen) {
window.requestAnimationFrame(() => {
document.addEventListener('click', clickOutsideHandler);
});
}
return () => {
document.removeEventListener('click', clickOutsideHandler);
};
}, [isOpen]);
Спасибо, это тот ответ, который я искал. В большинстве онлайн-примеров возврат используется в сочетании с версией монтирования компонента (с помощью []), чтобы что-то сделать, когда компонент размонтируется. Мне не совсем понятно, что возврат, например, [isOpen] вызывается всякий раз, когда isOpen собирается измениться на новое значение.
Не могли бы вы добавить минимально воспроизводимый пример ?