React项目首次点击按钮需两次点击才渲染新组件问题排查
Bug 原因定位
这个问题是React状态异步更新+闭包旧值导致的,具体触发逻辑:
- React会把同一个同步执行上下文里的所有
setState操作做批处理,状态值不会在调用setState后立刻变更,要等当前函数执行完、下一次渲染的时候才会更新。 - 你在
checkAnswer函数里先执行setQuestionNumber(questionNumber + 1),后面紧接着写setQuestion(questionsData.results[questionNumber].question)这类取新题的逻辑时,拿到的questionNumber还是当前渲染周期的旧值(也就是当前题的序号)。 - 首次加载完第一题(序号0)第一次点提交时:你把序号更新成1,但取新题全用的是序号0的内容,也就是当前正在答的题,页面看起来完全没变化;等第二次点提交时,
questionNumber已经是上次更新后的1,这时候取题用序号1才会渲染第二题,之后的流程看起来就正常了。 - 额外隐患:你直接通过DOM的
classList修改选项选中样式,不符合React数据驱动视图的设计,容易出现状态和UI不同步的问题;打乱选项时用splice直接修改接口返回的原数组,也存在数据污染的风险。
修复步骤
- 把换题的公共逻辑抽离,通过监听
questionNumber变化触发,避免在判断答对答错的分支里重复写相同逻辑,同时彻底解决闭包旧值问题 - 修正选项打乱逻辑,不要直接修改原数组
- 把选项选中的样式控制改成和
selectedAnswer状态绑定,不要直接操作DOM
修正后核心代码
首先修改fetchQuizData里的选项打乱逻辑,避免修改接口返回的原数组:
const fetchQuizData = async () => { const { data } = await axios.get("https://opentdb.com/api.php?amount=10"); setQuestionsData(data); const formattedData = data.results.map((item) => { // 浅拷贝错误答案数组,不修改接口返回的原数据 const answers = [...item.incorrect_answers]; const randomIndex = Math.floor(Math.random() * (answers.length + 1)); answers.splice(randomIndex, 0, item.correct_answer); return { ...item, answers }; }); setquizData(formattedData); // 初始化第一题数据 setCategory(data.results[0].category); setQuestion(data.results[0].question); setCorrectAnswer(data.results[0].correct_answer); setOptions(formattedData[0].answers); };
新增useEffect监听题号变化,统一处理换题逻辑,去掉重复代码:
// 监听题号变化自动加载对应题目 useEffect(() => { // 初始加载逻辑已在fetchQuizData处理,题号为0时不重复执行 if (questionNumber === 0 || !questionsData.results?.length || !quizData.length) return; // 答满10题跳结果页 if (questionNumber >= totalQuestion) { setQuizState("results"); return; } // 加载当前题号对应的题目内容 setQuestion(questionsData.results[questionNumber].question); setCategory(questionsData.results[questionNumber].category); setOptions(quizData[questionNumber].answers); setCorrectAnswer(questionsData.results[questionNumber].correct_answer); setSelectedAnswer(undefined); }, [questionNumber, questionsData, quizData, totalQuestion])
简化checkAnswer函数,移除重复的换题逻辑:
const checkAnswer = () => { // 未选择答案时弹出提示 if (selectedAnswer === undefined) { setShowAlert(true); return; } setShowAlert(false); // 答对则加分 if (selectedAnswer === correctAnswer) { setScore(prev => prev + 1); } // 无论答对答错,题号+1,触发上面的useEffect自动换题 setQuestionNumber(prev => prev + 1); };
修改选项渲染逻辑,用状态控制选中样式,移除手动操作DOM的代码:
{options.map((answer) => ( <div onClick={() => setSelectedAnswer(answer)} key={answer} value={answer} > <li // 选中状态直接和selectedAnswer绑定,不需要手动改class className={selectedAnswer === answer ? "options-selected" : "options"} dangerouslySetInnerHTML={createMarkup(answer)} ></li> </div> ))}
原selectAnswer函数里所有操作classList、prevSelectedElement的逻辑可以全部删除,prevSelectedElement这个状态也可以移除,选中状态完全由selectedAnswer控制即可。改完后首次点击提交就能正常切换题目,也不会出现UI和状态不同步的问题。
内容的提问来源于stack exchange,提问作者AlohaBombai
相关产品推荐
相关产品推荐

