Home > database >  Prevent react hooks countdown timer from resetting on click before it ends
Prevent react hooks countdown timer from resetting on click before it ends

Time:08-19

I've created a small quiz app that has a countdown timer. It currently resets when navigated to the next question. However, it is resetting the countdown on first click of the "next question" button before an answer is selected and resets at any point during the countdown. How can I keep the timer from resetting when clicking the "next question" button and before an option is selected?

function App() {
  let [points, setPoints] = useState(null);
  let [counter, setCounter] = useState(null);
  let [question, setQuestions] = useState();
  let [is_started, setStart] = useState(false);
  let [isCorrect, setIsCorrect] = useState(false);
  let [hasAnswered, setHasAnswered] = useState();
  let [isChecked, setChecked] = useState(null);
  let [showAnswer, setShowAnswer] = useState(false);
  let [is_end, setEnd] = useState(false);
  let [selectionMade, setSelectionMade] = useState();

  const getData = () => {
    fetch("./questions.json")
      .then(function (response) {
        return response.json();
      })
      .then(function (data) {
        setQuestions(data);
      });
  };
  useEffect(() => {
    getData();
  }, []);

  function Countdown({ seconds }) {
    const [timeLeft, setTimeLeft] = useState(seconds);
    const intervalRef = useRef;

    useEffect(() => {
      intervalRef.current = setInterval(() => {
        setTimeLeft((t) => t - 1);
      }, 1000);
      return () => clearInterval(intervalRef.current);
    }, []);

    useEffect(() => {
      if (timeLeft <= 0) {
        clearInterval(intervalRef.current);
        document.getElementById("false").checked = true;
        nextQuestion();
      }
    }, [timeLeft]);

    return <div>{timeLeft}s left to answer</div>;
  }

  function Answer(props) {
    return (
      <li aria-labelledby="answers-list">
        <label>
          <input
            type="radio"
            name="answer_group"
            className="answer"
            id={`${props.correct}`}
            value={props.answer}
            onChange={checkAnswer}
            checked={isChecked === props.answer}
            disabled={hasAnswered === true ? true : false}
          />
          {props.answer}
        </label>
      </li>
    );
  }

  function checkAnswer(e) {
    let val = e.target.value;
    let ans = question[counter].answers.filter((ans) => ans.value === val)[0];
    ans.correct === true ? setIsCorrect(true) : setIsCorrect(false);
    setHasAnswered(true);
    setChecked(ans.value);
  }

  function Starter(props) {
    return (
      <button className="" onClick={props.start}>
        {is_end ? "Restart" : "Start"}
      </button>
    );
  }

  function Quiz(props) {
    return (
      <div className="quiz-questions">
        <p className="question" id="questions">
          {props.question}
        </p>
        <ul className="answers" role="radiogroup" aria-labelledby="questions">
          {props.children}
        </ul>
      </div>
    );
  }

  function start() {
    setCounter(0);
    setPoints(0);
    setStart(!is_started);
    setEnd(false);
  }

  function nextQuestion() {
    setSelectionMade(true);
    setIsCorrect(false);
    setHasAnswered(false);
    setChecked(null);
    setShowAnswer(false);
    if (document.querySelector('input[name="answer_group"]:checked') == null) {
      setSelectionMade(false);
      return;
    }
    let val = document.querySelector('input[name="answer_group"]:checked')
      .value;
    let answerObj = question[counter].answers.filter(
      (ans) => ans.value === val
    )[0];
    let updated_points = answerObj.correct ? points   1 : points;
    setPoints(updated_points);
    let nextQuestion = counter   1;
    if (counter < question.length - 1) {
      setCounter(nextQuestion);
    } else {
      let today = new Date();
      let dd = String(today.getDate()).padStart(2, "0");
      let mm = String(today.getMonth()   1).padStart(2, "0");
      let yyyy = today.getFullYear();
      let hours = today.getHours();
      let AmOrPm = hours >= 12 ? "pm" : "am";
      hours = hours % 12 || 12;
      let minutes = ""   today.getMinutes();
      let minutesFixed = minutes < 10 ? "0"   minutes : minutes;
      let finalTime = hours   ":"   minutesFixed   " "   AmOrPm;
      let date = mm   "/"   dd   "/"   yyyy   " at "   finalTime;
      setEnd(!is_end);
      setStart(!is_started);
      setCounter(0);
      console.log({ date: date, score: updated_points });
      setTopScores([...topScores, { date: date, score: updated_points }]);
    }
  }

  return (
    <>
      <div
        className="Quiz slide-top"
        style={{ backgroundColor: "rgba(255,255,255,.5)" }}
      >
        {selectionMade === false && hasAnswered === false ? (
          <div className="notification swing-in-top-fwd">
            <p>Please select an answer to continue</p>
          </div>
        ) : (
          ""
        )}
        {!is_started ? (
          <div className="quiz-intro">
            <div className="start-intro-wrapper">
              <Starter start={start} />
            </div>
          </div>
        ) : (
          <div className="quick-wrapper slide-in-bottom">
            {!isChecked && <Countdown seconds={15} />}
            <div className="status-details"></div>
            <Quiz question={question[counter].question}>
              <ul>
                {question[counter].answers.map((answer, index, arr) => {
                  return (
                    <Answer
                      key={index}
                      index={index}
                      answer={answer.value}
                      correct={answer.correct}
                    />
                  );
                })}
              </ul>
            </Quiz>
            <div className="answer-controls">
              <div className="answer-buttons">
                <button
                  className="button"
                  onClick={(e) => nextQuestion()}
                >
                  {isChecked === null ? "Select an answer" : "Next question"}
                </button>
              </div>
            </div>
          </div>
        )}
      </div>
    </>
  );
}

export default App;

Codesandbox link

CodePudding user response:

I would display buttons conditionally and remove onClick if isChecked is null.

<div className="answer-buttons">
    {isChecked === null ? 
        <button className="">Select an answer</button>
     
    : 
    
        <button className="" onClick={(e) => nextQuestion()}>
            Next question
        </button>
    }
</div>

Edited Sandbox

CodePudding user response:

You have a few issues to fix here... the main problem is that you're defining components inside components - so each time App renders (e.g. because of a state change), it will recreate a new Countdown component, which will reset the timer.

If you pull the countdown out to its own component -

const Countdown = ({ seconds, onNextQuestion }) => {
  const [timeLeft, setTimeLeft] = useState(seconds);
  const intervalRef = useRef();

  useEffect(() => {
    intervalRef.current = setInterval(() => {
      setTimeLeft((t) => t - 1);
    }, 1000);

    return () => clearInterval(intervalRef.current);
  }, []);

  useEffect(() => {
    if (timeLeft <= 0) {
      clearInterval(intervalRef.current);
      document.getElementById("false").checked = true;
      onNextQuestion();
    }
  }, [timeLeft, onNextQuestion]);

  return <div>{timeLeft}s left to answer</div>;
}

And then wire that in:

<Countdown seconds={15} onNextQuestion={nextQuestion} />

Then it will fix the problem of it resetting when you hit "next". You should ideally do that with all the parts of your App that could stand-aone.

Here's an updated CodeSandbox showing the solution above.

CodePudding user response:

I have updated several parts of your code. Please have a look here. Hope this would be helpful for you.

  • Related