Функция очистки React useEffect не отменяет изменения, вызванные строгим режимом React в разработке

Эта проблема связана с тем, что 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">
                        &times; {/* 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;
    }
}

ваша конечная цель - закрыть это, щелкнув за пределами этого, я думаю, вы сможете использовать хук useRef, чтобы сделать это гораздо проще, если это то, чего вы пытаетесь достичь

justnut 15.07.2024 19:56

Да. Это конечная цель.

onyedikachi23 15.07.2024 20:52
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Навигация по приложениям React: Исчерпывающее руководство по React Router
Навигация по приложениям React: Исчерпывающее руководство по React Router
React Router стала незаменимой библиотекой для создания одностраничных приложений с навигацией в React. В этой статье блога мы подробно рассмотрим...
Массив зависимостей в React
Массив зависимостей в React
Все о массиве Dependency и его связи с useEffect.
1
2
61
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

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

    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?

onyedikachi23 15.07.2024 20:52

лол, извини, это было именно то, на что я ссылался в моем коде. обновлено сейчас, дайте мне знать, если вам нужны дополнительные разъяснения

justnut 15.07.2024 21:32

Я попробовал интегрировать ваш подход с родительским компонентом, но получил тот же результат. Не могли бы вы попробовать интегрировать его самостоятельно и посмотреть, работает ли он или требует некоторых исправлений?

onyedikachi23 16.07.2024 09:37

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

justnut 17.07.2024 04:35

Думаю, я решил проблему, используя event.stopPropagation() . Посмотрите мой ответ

onyedikachi23 17.07.2024 05:49

Я думаю, вы неправильно понимаете концепцию функции очистки. Вы обязаны написать состояние, эффекты и взаимодействие таким образом, чтобы, когда все будет перемонтировано и все эффекты сделают свое дело, вы вернетесь к исходной точке. В вашем случае вы подключаете прослушиватель событий и меняете состояние, но затем при очистке вы только удаляете прослушиватель и не трогаете состояние. Это значит, что вы не все убрали.

Другое дело, что гораздо проще было бы реализовать вообще без эффектов. Модальный диалог является «модальным», что означает, что пользователь фактически не может взаимодействовать со страницей под ним. Простое решение — сделать наложение над видимым содержимым (оно может быть прозрачным, если вы хотите, чтобы оно «отсутствовало») и слушать щелчок по нему (тот же, что и по кнопке). Таким образом, для управления модальным состоянием вам понадобится одна логическая переменная состояния и один обработчик для изменения этой переменной.

Ответ принят как подходящий

На основе рекомендаций @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">
                        &times; {/* 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 компонентом, не похоже, что его будут использовать повторно, но если это хорошо

justnut 18.07.2024 06:45

Если модальное окно не отображается, больше не нужно запускать handleClickOutside() с помощью событий щелчка по элементу документа; функция очистки помогает, когда компонент удаляется, когда модальное окно все еще отображается. Мне нужно, чтобы пользователь нажал и отпустил, прежде чем появится модальное окно, поэтому я использую щелчок.

onyedikachi23 21.07.2024 17:29

Пока вы знаете, что все хорошо :) Ну, поскольку зависимость isModalPopedUp, очистка будет запущена, когда она закроется, что делает else ненужным, но вам все равно понадобится if, если я ошибаюсь, все выглядит хорошо :)

justnut 22.07.2024 19:12

Я понимаю вашу точку зрения, но я оставляю код таким.

onyedikachi23 23.07.2024 02:11

Другие вопросы по теме