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

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直接修改接口返回的原数组,也存在数据污染的风险。
修复步骤
  1. 把换题的公共逻辑抽离,通过监听questionNumber变化触发,避免在判断答对答错的分支里重复写相同逻辑,同时彻底解决闭包旧值问题
  2. 修正选项打乱逻辑,不要直接修改原数组
  3. 把选项选中的样式控制改成和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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.29 05:45:37