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
相关产品推荐
相关产品推荐

