如何阻止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
相关产品推荐
相关产品推荐

