You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何阻止React Hooks倒计时器在未结束时因点击提前重置?

问题

我开发了一个带倒计时器的小型测验应用,切换至下一题时计时器会重置。但存在两个问题:

  • 未选择答案时点击“下一题”按钮,倒计时器会提前重置
  • 倒计时过程中任意点击都会触发重置

需要实现:点击“下一题”按钮且未选中选项时,保持倒计时器不重置。

解决方案

核心思路是仅当用户完成答题(选中选项或倒计时结束)时,才允许切换题目并重置计时器,同时避免在未选答案时执行重置相关的状态更新。具体修改如下:

1. 修复nextQuestion函数的执行逻辑

当前nextQuestion一开始就执行了setChecked(null)等状态重置操作,这会导致计时器因!isChecked条件重新渲染(即重置)。需将状态重置逻辑移到确认用户已选答案之后:

function nextQuestion() {
  // 先检查是否未选择答案
  const selectedAnswer = document.querySelector('input[name="answer_group"]:checked');
  if (selectedAnswer == null) {
    setSelectionMade(false);
    return; // 直接返回,不执行后续状态重置和切换逻辑
  }

  // 已选答案,再执行状态重置
  setSelectionMade(true);
  setIsCorrect(false);
  setHasAnswered(false);
  setChecked(null);
  setShowAnswer(false);

  // 计算分数和切换题目
  let val = selectedAnswer.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 }]);
  }
}

2. 优化Countdown组件的依赖与初始化逻辑

当前Countdown组件的intervalRef初始化有误,且缺少必要的依赖项,修改后确保计时器正确启动和清理:

function Countdown({ seconds }) {
  const [timeLeft, setTimeLeft] = useState(seconds);
  const intervalRef = useRef(null); // 正确初始化useRef为null

  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, nextQuestion]); // 添加nextQuestion到依赖,避免lint警告
}

3. 补充缺失的状态定义

原代码中使用了topScores但未定义,需补充状态声明:

let [topScores, setTopScores] = useState([]);

修改后的完整代码

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();
  let [topScores, setTopScores] = 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(null);

    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, nextQuestion]);

    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() {
    const selectedAnswer = document.querySelector('input[name="answer_group"]:checked');
    if (selectedAnswer == null) {
      setSelectionMade(false);
      return;
    }

    setSelectionMade(true);
    setIsCorrect(false);
    setHasAnswered(false);
    setChecked(null);
    setShowAnswer(false);

    let val = selectedAnswer.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;

内容的提问来源于stack exchange,提问作者Matt

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.22 04:24:16