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

React useEffect依赖数组异常触发请求及选项乱序问题排查

问题分析与解决方案

核心问题1:点击Check Answer后页面提前重新渲染,无法查看选项对错

原因

你将playAgain放入useEffect的依赖数组,点击Check Answer时调用displayAnswer()会执行setPlayAgain(true),触发useEffect重新请求数据并更新quiz状态,导致页面直接渲染新题目,原题目和选项被替换,自然无法查看对错标记。

核心问题2:点击正确选项时选项重新打乱

原因

每次组件重新渲染(比如userAnswer更新时),quizElements里的createRandomOptions都会重新执行,生成新的随机选项顺序;同时checkEachAnswer函数定义在map回调内部,每次渲染都会重新创建,也会触发不必要的重渲染,最终导致选项顺序变化。


解决方案

针对问题1:分离重新请求数据的触发逻辑

  • 将初始化请求和Play Again的请求逻辑拆分,useEffect仅在组件挂载时请求一次数据;
  • 给Play Again按钮绑定单独的请求函数,不再依赖playAgain状态触发重新请求。

针对问题2:提前固化随机选项顺序

在获取到quiz数据后,立即为每个题目生成随机选项并保存到状态中,而非每次渲染时临时生成,确保选项顺序稳定。


修改后的完整代码

Quiz组件

export default function Quiz() {
  const [quiz, setQuiz] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(false);
  const [userAnswer, setUserAnswer] = useState([]);
  const [showAnswer, setShowAnswer] = useState(false);
  const [showAnswerBtn, setShowAnswerBtn] = useState(true);

  // 抽离随机选项生成函数,避免重复创建
  const createRandomOptions = (arr) => {
    let copyOptions = [...arr];
    let randomOptionsArr = [];
    while (copyOptions.length > 0) {
      let randomIndex = Math.floor(Math.random() * copyOptions.length);
      randomOptionsArr.push(copyOptions[randomIndex]);
      copyOptions.splice(randomIndex, 1);
    }
    return randomOptionsArr;
  };

  // 初始化仅请求一次数据
  useEffect(() => {
    fetchQuizData();
  }, []);

  // 单独的获取数据函数,供Play Again调用
  const fetchQuizData = () => {
    setLoading(true);
    fetch("https://opentdb.com/api.php?amount=5&category=18&difficulty=hard&type=multiple")
      .then(result => {
        if (!result.ok) {
          throw new Error("HTTP error", { cause: result.status });
        }
        return result.json();
      })
      .then(data => {
        // 提前处理每个题目的随机选项,存入状态
        const processedQuiz = data.results.map(item => ({
          ...item,
          randomOptions: createRandomOptions([...item.incorrect_answers, item.correct_answer])
        }));
        setQuiz(processedQuiz);
      })
      .catch(error => {
        console.error("获取数据失败:", error);
        setQuiz(null);
        setError(true);
      })
      .finally(() => {
        setLoading(false);
      });
  };

  // 抽离选项点击逻辑,避免重复创建函数
  const handleOptionClick = (option, correctOption, questionIndex) => {
    if (showAnswer) return; // 展示答案后禁止点击
    setUserAnswer(prev => {
      const newAnswers = [...prev];
      // 按题目索引存储答案,避免重复添加
      newAnswers[questionIndex] = option === correctOption ? option : null;
      return newAnswers;
    });
  };

  const displayAnswer = () => {
    setShowAnswer(true);
    setShowAnswerBtn(false);
  };

  const handlePlayAgain = () => {
    setShowAnswer(false);
    setShowAnswerBtn(true);
    setUserAnswer([]);
    fetchQuizData(); // 点击时主动请求新数据
  };

  // 计算得分
  const score = userAnswer.filter(Boolean).length;

  return (
    <>
      {loading && <h3>加载中...</h3>}
      {error && <h3>获取数据出错了!</h3>}
      {quiz && <h1 className="topic">主题: 计算机科学</h1>}
      
      {quiz && quiz.map((eachQuiz, questionIndex) => (
        <div key={eachQuiz.question} className="quiz-wrapper">
          <p className="question">{eachQuiz.question}</p>
          <ul>
            {eachQuiz.randomOptions.map((option) => {
              const isCorrect = option === eachQuiz.correct_answer;
              const isSelected = userAnswer[questionIndex] === option;
              // 根据状态设置类名,方便样式区分对错
              let optionClass = "option";
              if (showAnswer) {
                optionClass += isCorrect ? " correct" : " incorrect";
              } else if (isSelected) {
                optionClass += " selected";
              }

              return (
                <li
                  key={option}
                  className={optionClass}
                  onClick={() => handleOptionClick(option, eachQuiz.correct_answer, questionIndex)}
                >
                  {option}
                </li>
              );
            })}
          </ul>
        </div>
      ))}

      {showAnswer && <p>你的得分: {score} / {quiz?.length || 0}</p>}

      {quiz && showAnswerBtn && (
        <button onClick={displayAnswer} className="main-btn">
          查看答案
        </button>
      )}

      {quiz && showAnswer && (
        <button onClick={handlePlayAgain} className="main-btn">
          再来一局
        </button>
      )}
    </>
  );
}

App组件(修复key无效问题)

原代码中Quiz的key生成逻辑无效,修改后如下:

import { useState } from "react";
import Quiz from "./components/Quiz";

export default function App() {
  const [startQuiz, setStartQuiz] = useState(false);

  const loadQuiz = () => {
    setStartQuiz(true);
  };

  return (
    <>
      {!startQuiz && (
        <div className="intro-container">
          <div className="intro-screen">
            <h1>Quiz My Heart</h1>
            <button className="start-btn" onClick={loadQuiz}>
              开始答题
            </button>
          </div>
        </div>
      )}

      {startQuiz && (
        <main>
          <Quiz />
        </main>
      )}
    </>
  );
}

关键优化点说明

  • 抽离工具函数和点击逻辑,避免每次渲染重复创建函数,减少不必要重渲染;
  • 获取数据时提前生成并保存随机选项,确保选项顺序稳定;
  • 用题目索引对应答案,优化userAnswer的存储逻辑;
  • 添加选项状态类名,支持展示选中、正确、错误的视觉反馈;
  • 拆分数据请求逻辑,仅在初始化和Play Again时触发请求,避免Check Answer时误刷新题目。

内容的提问来源于stack exchange,提问作者Hi - P

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.13 08:37:33