Эта проблема связана с тем, что React намеренно перемонтирует ваши компоненты в разработке, чтобы найти ошибки.
Я знаю, что могу отключить строгий режим, но React не рекомендует это делать. Я просто ищу рекомендуемый подход, чтобы это исправить.
Я пытаюсь создать открытый модальный компонент, который закрывается либо при нажатии кнопки закрытия, либо при нажатии пользователем за пределами модального содержимого.
Итак, чтобы реализовать закрытие модального окна внешним щелчком, я использую перехватчик useEffect, и я хочу, чтобы его функция обратного вызова запускалась только при изменении состояния isModalPopedUp и не запускалась при первоначальном рендеринге компонента. Для этого я использую специальный хук:
import { useRef } from "react";
import { useEffect } from "react";
// This custom hook sets if a useEffect callback runs at initial render or not
export default function useDidMountEffect(argObject = {}) {
/*
argObject = {
runOnInitialRender: boolean,
callback: () => void,
dependencies: any[] // array of dependencies for useEffect
}
*/
const didMountRef = useRef(argObject.runOnInitialRender);
// useEffect to run
useEffect(() => {
if (didMountRef.current) {
// callback will run on first render if argObject.runOnInitialRender is true
argObject.callback();
} else {
// callback will now run on dependencies change
didMountRef.current = true;
}
}, argObject.dependencies); // only run if dependencies change
}
Что касается модального компонента, я разделил его на два компонента: родительский-модальный, который обрабатывает логику состояния:
import { useEffect, useState } from "react";
import Modal from "./modal";
import useDidMountEffect from "./useDidMountHook";
import "./modal.css";
export default function ModalParent() {
const [isModalPopedUp, setIsModalPopedUp] = useState(false);
function handleToggleModalPopup() {
setIsModalPopedUp((prevState) => !prevState);
}
function handleOnClose() {
setIsModalPopedUp(false);
}
function handleOutsideModalClick(event) {
!event.target.classList.contains("modal-content") && handleOnClose();
}
// add event listener for outside modal click using the useDidMountEffect hook
useDidMountEffect({
runOnInitialRender: false,
callback: () => {
// add event listener when modal is shown
if (isModalPopedUp) {
document.addEventListener("click", handleOutsideModalClick);
} else {
// remove event listener when modal is closed
document.removeEventListener("click", handleOutsideModalClick);
}
// return a cleanup function that removes the event listener when component unmounts
return () => {
document.removeEventListener("click", handleOutsideModalClick);
};
},
dependencies: [isModalPopedUp], // only re-run the effect when isModalPopedUp changes
});
return (
<div>
<button
onClick = {() => {
handleToggleModalPopup();
}}>
Open Modal Popup
</button>
{isModalPopedUp && (
<Modal
header = {<h1>Customised Header</h1>}
footer = {<h1>Customised Footer</h1>}
onClose = {handleOnClose}
body = {<div>Customised Body</div>}
/>
)}
</div>
);
}
и основной модальный компонент:
export default function Modal({ id, header, body, footer, onClose }) {
return (
<div id = {id || "modal"} className = "modal">
<div className = "modal-content">
<div className = "header">
<span onClick = {onClose} className = "close-modal-icon">
× {/* an X icon */}
</span>
<div>{header ? header : "Header"}</div>
</div>
<div className = "body">
{body ? (
body
) : (
<div>
<p>This is our Modal body</p>
</div>
)}
</div>
<div className = "footer">
{footer ? footer : <h2>footer</h2>}
</div>
</div>
</div>
);
}
Итак, проблема в том, что React перемонтирует родительский компонент после первоначального рендеринга, выполняя обратный вызов для useDidMountEffect, чтобы немедленно добавить прослушиватель событий щелчка к элементу document
без изменения состояния isModalPopedUp на true при нажатии кнопки «открыть модальное».
Таким образом, при нажатии на кнопку «открыть модальное окно» isModalPopedUp переключается на true, но затем сразу же меняется на false из-за преждевременного добавления прослушивателя событий щелчка в документ. Таким образом, в конечном итоге модальное окно невозможно открыть, нажав кнопку «Открыть модальное окно».
React.dev предлагает простое решение, используя функцию очистки, возвращаемую обратным вызовом useEffect, чтобы отменить изменения после перемонтирования:
Обычно ответом является реализация функции очистки. Функция очистки должна остановить или отменить все действия Эффекта. Эмпирическое правило заключается в том, что пользователь не должен иметь возможности различать эффект, запускаемый один раз (как в рабочей среде), и последовательность настройки → очистки → настройки (как вы видите при разработке).
Моя функция очистки удаляет прослушиватель событий щелчка из элемента document
, но это все равно не решает проблему.
Для оформления модального окна я использую:
/** @format */
.modal {
position: fixed;
z-index: 1;
padding-top: 2rem;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: hidden;
background-color: #b19b9b;
color: black;
overflow: auto;
padding-bottom: 2rem;
}
.modal-content {
position: relative;
background-color: #fefefe;
margin: auto;
padding: 0;
border: 1px solid red;
width: 80%;
animation-name: animateModal;
animation-duration: 0.5s;
animation-timing-function: ease-in-out;
}
.close-modal-icon {
cursor: pointer;
font-size: 40px;
position: absolute;
top: 0.5rem;
right: 0.5rem;
font-weight: bold;
}
.header {
padding: 4px 10px;
background-color: #5cb85c;
color: white;
}
.body {
padding: 2px 16px;
height: 200px;
}
.footer {
padding: 4px 16px;
background-color: #5cb85c;
color: white;
}
@keyframes animateModal {
from {
top: -200px;
opacity: 0;
}
to {
top: 0;
opacity: 1;
}
}
Да. Это конечная цель.
Следующее поможет вам настроить то, что вам нужно для обработки щелчков за пределами модального окна.
const modalRef = useRef<HTMLDivElement>(null);
const [isModalOpen, setIsModalOpen] = useState(false);
const handleClickOutside = (e: MouseEvent) => {
if (modalRef.current && !modalRef.current.contains(e.target as Node)){
setIsModalOpen(false);
}
};
Наряду с этим вам нужно будет установить ссылку и внутри useEffect сделать что-то вроде этого:
useEffect(() => {
if (isModalOpen){
window.addEventListener('mousedown', handleClickOutside)
}
}, [isModalOpen]);
Что за commandBarRef
?
лол, извини, это было именно то, на что я ссылался в моем коде. обновлено сейчас, дайте мне знать, если вам нужны дополнительные разъяснения
Я попробовал интегрировать ваш подход с родительским компонентом, но получил тот же результат. Не могли бы вы попробовать интегрировать его самостоятельно и посмотреть, работает ли он или требует некоторых исправлений?
можете ли вы отредактировать сообщение, чтобы показать мне текущую итерацию, чтобы я мог видеть
Думаю, я решил проблему, используя event.stopPropagation() . Посмотрите мой ответ
Я думаю, вы неправильно понимаете концепцию функции очистки. Вы обязаны написать состояние, эффекты и взаимодействие таким образом, чтобы, когда все будет перемонтировано и все эффекты сделают свое дело, вы вернетесь к исходной точке. В вашем случае вы подключаете прослушиватель событий и меняете состояние, но затем при очистке вы только удаляете прослушиватель и не трогаете состояние. Это значит, что вы не все убрали.
Другое дело, что гораздо проще было бы реализовать вообще без эффектов. Модальный диалог является «модальным», что означает, что пользователь фактически не может взаимодействовать со страницей под ним. Простое решение — сделать наложение над видимым содержимым (оно может быть прозрачным, если вы хотите, чтобы оно «отсутствовало») и слушать щелчок по нему (тот же, что и по кнопке). Таким образом, для управления модальным состоянием вам понадобится одна логическая переменная состояния и один обработчик для изменения этой переменной.
На основе рекомендаций @justnut и @Mr. Ежик, мне удалось придумать рабочее решение.
Проблема вызвана тем, что прослушиватель событий щелчка устанавливается на элементе document
преждевременно при нажатии кнопки «открыть модальное окно» (из-за способа работы ловушки useEffect).
Итак, при нажатии кнопки «открыть модальное окно» событие всплывает/распространяется до элемента document
, тем самым неожиданно запуская функциюhandOutsideClick.
Таким образом, исправление состоит в том, чтобы остановить распространение события до элемента document
с помощью метода event.stopPropagation() в обратном вызове события нажатия кнопки «открыть модальное окно».
Чтобы проверить, был ли щелчок за пределами модального окна, я использовал подход @justnut. По материалам @Mr. Hedgehog объяснений, я вернул лучшую функцию очистки в хуке useEffect.
Это общий измененный код в родительско-модальном компоненте (useDidMountEffect удален):
/** @format */
import { useEffect, useRef, useState } from "react";
import Modal from "./modal";
import "./modal.css";
export default function ModalParent() {
const [isModalPopedUp, setIsModalPopedUp] = useState(false);
function handleToggleModalPopup() {
setIsModalPopedUp((prevState) => !prevState);
}
function handleOnClose() {
setIsModalPopedUp(false);
}
function handleClickOutside(event) {
if (modalRef.current && !modalRef.current.contains(event.target))
handleOnClose();
}
// create modalRef for handling outside modal click events
const modalRef = useRef(null);
// create a useEffect for handling outside modal click events
useEffect(() => {
if (isModalPopedUp) {
window.addEventListener("click", (event) => {
handleClickOutside(event);
});
} else {
// Remove the event listener when modal is closed
window.removeEventListener("click", handleClickOutside);
}
// Cleanup: Remove the event listener when component unmounts
return () => {
window.removeEventListener("click", handleClickOutside);
};
}, [isModalPopedUp]);
return (
<div>
<button
onClick = {(event) => {
event.stopPropagation();
// toggle modal popup state
handleToggleModalPopup();
}}>
Open Modal Popup
</button>
{isModalPopedUp && (
<Modal
header = {<h1>Customised Header</h1>}
footer = {<h1>Customised Footer</h1>}
onClose = {handleOnClose}
body = {<div>Customised Body</div>}
modalRef = {modalRef}
/>
)}
</div>
);
}
и небольшое дополнение к основному модальному компоненту, куда я добавил свойство modalRef:
/** @format */
export default function Modal({ id, header, body, footer, onClose, modalRef }) {
return (
<div id = {id || "modal"} className = "modal">
<div className = "modal-content" ref = {modalRef}>
<div className = "header">
<span onClick = {onClose} className = "close-modal-icon">
× {/* an X icon */}
</span>
<div>{header ? header : "Header"}</div>
</div>
<div className = "body">
{body ? (
body
) : (
<div>
<p>This is our Modal body</p>
</div>
)}
</div>
<div className = "footer">
{footer ? footer : <h2>footer</h2>}
</div>
</div>
</div>
);
}
Помощь: Если кто-нибудь сможет превратить это в работоспособный фрагмент стека реакции, я буду очень признателен. Модальный компонент и CSS для стилизации описаны в вопросе выше.
Вам не понадобится } else { // Удалить прослушиватель событий, когда модальное окно закрыто window.removeEventListener("click", handleClickOutside); } функция очистки должна позаботиться об этом за вас. Я бы также рекомендовал «нажатие мыши» вместо «щелчка», щелчок, я думаю, справится с нажатием и отпусканием, что может вызвать некоторые проблемы, в то время как «нажатие мыши» - это просто нажатие. Я бы также просто не делал здесь Modal компонентом, не похоже, что его будут использовать повторно, но если это хорошо
Если модальное окно не отображается, больше не нужно запускать handleClickOutside()
с помощью событий щелчка по элементу документа; функция очистки помогает, когда компонент удаляется, когда модальное окно все еще отображается. Мне нужно, чтобы пользователь нажал и отпустил, прежде чем появится модальное окно, поэтому я использую щелчок.
Пока вы знаете, что все хорошо :) Ну, поскольку зависимость isModalPopedUp, очистка будет запущена, когда она закроется, что делает else ненужным, но вам все равно понадобится if, если я ошибаюсь, все выглядит хорошо :)
Я понимаю вашу точку зрения, но я оставляю код таким.
ваша конечная цель - закрыть это, щелкнув за пределами этого, я думаю, вы сможете использовать хук useRef, чтобы сделать это гораздо проще, если это то, чего вы пытаетесь достичь