skip to Main Content

I’m building a React quiz app where a modal should appear once all answers are correct or when the user loses all the lives. However, the modal opens twice instead of once despite my attempts to control the state. I tried managing the modal display using a state variable showModal. I’ve also attempted adding a flag to control it but the issue persists. Added some console.log and i saw that once i open the page the modal is being rendered multiple times already. What could be causing this issue?

const handleModalClose = () => {
  setShowModal(false);
};

const Modal = ({ isOpen, onClose }) => {
  const modalRef = useRef();

  useEffect(() => {
    const handleClickOutside = (event) => {
      if (modalRef.current && !modalRef.current.contains(event.target)) {
        onClose();
      }
    };

    document.addEventListener("mousedown", handleClickOutside);

    return () => {
      document.removeEventListener("mousedown", handleClickOutside);
    };
  }, [onClose]);

  if (!isOpen) return null;
  if (correctAnswersCount >= 9) {
    confetti({
      particleCount: 100,
      spread: 70,
      origin: { y: 0.6 },
    });
  }

  return (
    <div className="modal-overlay" onClick={onClose}>
      <div className="modal-content" ref={modalRef}>
        {correctAnswersCount > 9 && (
          <h2>Congrats! You've found all the correct answers!</h2>
        )}
        <h3>
          <b>Score</b>
        </h3>

        <div className="answer-progress-container">
          <div className="answer-progress">
            <div
              className="answer-progress-bar"
              style={{ width: `${(correctAnswersCount / 10) * 100}%` }}
            >
              {correctAnswersCount}/10
            </div>
          </div>
          <p className="streak-p">Current Streak: {streakCounter}</p>
        </div>
      </div>
      <button className="modal-close" onClick={onClose}>
        x
      </button>

The part where i call the modal, hope it’s clearer now;

{console.log("Modal rendered")}
{showModal && (
        <Modal
          isOpen={showModal}
          onClose={handleModalClose}
        />
      )}

2

Answers


  1. One option would be to use useEffect to check the correctAnswersCount and
    lives:

    useEffect(() => {
      // assuming you also have state called 'lives'
      if (correctAnswersCount >= 9 || lives === 0) {
        setShowModal(true);
      }
    }, [correctAnswersCount, lives]);
    
    // other parts of the code
    

    So,

    // Get a hook function
    const { useState, useRef, useEffect } = React;
    
    const Modal = ({ isOpen, onClose, correctAnswersCount, lives }) => {
        const modalRef = useRef();
    
        useEffect(() => {
            const handleClickOutside = (event) => {
                if (modalRef.current && !modalRef.current.contains(event.target)) {
                    onClose();
                }
            };
    
            document.addEventListener("mousedown", handleClickOutside);
    
            return () => {
                document.removeEventListener("mousedown", handleClickOutside);
            };
        }, [onClose]);
    
        return (
            <div className="modal-overlay" onClick={onClose}>
                <div className="modal-content" ref={modalRef}>
                    {correctAnswersCount > 9 && (
                        <h2>Congrats! You've found all the correct answers!</h2>
                    )}
                    <h3>
                        <b>Score</b>
                    </h3>
    
                    <div className="answer-progress-container">
                        <div className="answer-progress">
                            <div
                                className="answer-progress-bar"
                                style={{ width: `${(correctAnswersCount / 10) * 100}%` }}
                            >
                                {correctAnswersCount}/10
                            </div>
                        </div>
                    </div>
                </div>
                <button className="modal-close" onClick={onClose}>
                    x
                </button>
            </div>);
    }
    
    const App = ({ title }) => {
        const [correctAnswersCount, setCorrectAnswersCount] = useState(7);
        const [lives, setLives] = useState(0);
        const [showModal, setShowModal] = useState(false);
    
        const handleModalClose = () => {
            // handle closing modal
        }
    
        useEffect(() => {
            // assuming you also have state called 'lives'
            if (correctAnswersCount >= 9 || lives === 0) {
                setShowModal(true);
            }
        }, [correctAnswersCount, lives]);
        return (
            <div>
                <button onClick={() => setCorrectAnswersCount(correctAnswersCount + 1)}>Counter</button>
                {showModal && (
                    <Modal
                        isOpen={showModal}
                        onClose={handleModalClose}
                        correctAnswersCount={correctAnswersCount}
                        lives={lives}
                    />
                )}
            </div>
        );
    };
    
    ReactDOM.createRoot(
        document.getElementById("root")
    ).render(
        <App />
    );
    <div id="root"></div>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.development.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.development.js"></script>
    Login or Signup to reply.
  2. There is no chance for any part of the code inside the component Modal to cause this bug by appearing twice. Below is the reasoning for this conclusion.

    1. This component does not change the state showModal with multiple values. It means it sets the state only to one value – false.
    2. This component itself is not a recursive one.

    Therefore the calling site should be the only cause of failure. This component must be rendered somewhere else in the calling site besides the following statement.

      {showModal && <Modal isOpen={showModal} onClose={handleModalClose} />}
    

    Please check again the calling site in this direction.

    Please see below a sample code based on the same component. It does not have the reported issue. And it is to repeat that the only change in the below code is the calling site. The calling site below does not have rendered the component not more than once.

    Please note that the state modalDismissed used in the sample code is used just for an alert to be shown, for the sake of clarity of this sample. It does not have any more relevance in the code.

    App.js

    import { useState, useRef, useEffect } from 'react';
    
    export default function App() {
      const [showModal, setShowModal] = useState(false);
      const [modalDismissed, setModalDismissed] = useState(false);
    
      const handleModalClose = () => {
        setShowModal(false);
        setModalDismissed(true);
      };
    
      function handleModalShow() {
        if (modalDismissed) {
          alert('Model has been shown already once');
          return;
        }
        setShowModal(true);
      }
    
      return (
        <>
          <button onClick={handleModalShow}>Show Modal Once</button>
          {showModal && <Modal isOpen={showModal} onClose={handleModalClose} />}
        </>
      );
    }
    
    const Modal = ({ isOpen, onClose }) => {
      const correctAnswersCount = 0;
      const streakCounter = 0;
    
      const modalRef = useRef();
    
      useEffect(() => {
        const handleClickOutside = (event) => {
          if (modalRef.current && !modalRef.current.contains(event.target)) {
            onClose();
          }
        };
    
        document.addEventListener('mousedown', handleClickOutside);
    
        return () => {
          document.removeEventListener('mousedown', handleClickOutside);
        };
      }, [onClose]);
    
      if (!isOpen) return null;
      if (correctAnswersCount >= 9) {
        confetti({
          particleCount: 100,
          spread: 70,
          origin: { y: 0.6 },
        });
      }
    
      return (
        <div className="modal-overlay" onClick={onClose}>
          <div className="modal-content" ref={modalRef}>
            {correctAnswersCount > 9 && (
              <h2>Congrats! You've found all the correct answers!</h2>
            )}
            <h3>
              <b>Score</b>
            </h3>
    
            <div className="answer-progress-container">
              <div className="answer-progress">
                <div
                  className="answer-progress-bar"
                  style={{ width: `${(correctAnswersCount / 10) * 100}%` }}
                >
                  {correctAnswersCount}/10
                </div>
              </div>
              <p className="streak-p">Current Streak: {streakCounter}</p>
            </div>
          </div>
          <button className="modal-close" onClick={onClose}>
            x
          </button>
        </div>
      );
    };
    

    Test run:

    a) On load of the App

    Browser display On load of the App

    b) On the first click of the button

    Browser display on the first click of the button

    c) On dismissing the Modal by clicking the button X

    Browser display On dismissing the Modal by clicking the button X

    d) On the second click of the button

    Browser display On the second click of the button

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search