React问答应用传递setter至子组件的功能异常求助
React问答应用问题修复方案
核心问题梳理与修复
1. 点击答案后按钮样式失效、答案重打乱、分数重复累加
问题根源
- App组件每次重渲染时,都会重新执行
shuffle生成新的答案数组,并且用nanoid()作为Question组件的key,导致组件被强制卸载重建,之前的选中状态全部丢失。 - Question组件直接通过DOM操作添加类名,组件重建后样式被重置;同时点击正确答案时设置
setCorrectAnswer(false),导致正确答案的绿色样式逻辑不触发。 - 未限制用户重复点击同一答案,导致分数多次累加。
修复方案
- 在App组件中,将处理好的问题(包含打乱后的答案)存入state,避免每次渲染重复生成。
- 使用稳定的key(比如问题文本),保证Question组件不会被频繁重建。
- 在Question组件中用状态管理每个答案的选中状态,禁止重复点击,通过状态控制样式而非直接操作DOM。
2. 倒计时功能异常
问题根源
原倒计时useEffect依赖counter,每次counter变化都会创建新的定时器,导致倒计时速度越来越快,并且组件卸载时可能出现清理不及时的问题。
修复方案
使用函数式更新setCounter,配合ref保存定时器ID,确保定时器只创建一次,并且在组件卸载时正确清理;同时切换问题时重置倒计时。
修改后的完整代码
App.js
import React from 'react'; import './style.css'; import Header from './components/Header' import Question from './components/Question' import axios from 'axios'; function App() { const [questions, setQuestions] = React.useState([]) const [currentQuestionIndex, setCurrentQuestionIndex] = React.useState(0) const [counter, setCounter] = React.useState(30) const [score, setScore] = React.useState(0) const timerRef = React.useRef(null) // 洗牌函数,避免修改原数组 function shuffle(array) { const newArray = [...array] for (let i = newArray.length - 1; i > 0; i--) { let j = Math.floor(Math.random() * (i + 1)); [newArray[i], newArray[j]] = [newArray[j], newArray[i]] } return newArray; } // 获取问题并预处理答案 React.useEffect(() => { axios.get('https://opentdb.com/api.php?amount=10') .then((res) => { const processedQuestions = res.data.results.map(q => { // 组合正确和错误答案,添加是否正确的标记 const answers = [ { text: q.correct_answer, isCorrect: true }, ...q.incorrect_answers.map(text => ({ text, isCorrect: false })) ] return { ...q, shuffledAnswers: shuffle(answers) } }) setQuestions(processedQuestions) }) }, []) // 倒计时逻辑 React.useEffect(() => { // 每次切换问题重置倒计时 setCounter(30) // 清理之前的定时器 if (timerRef.current) clearInterval(timerRef.current) timerRef.current = setInterval(() => { setCounter(prev => { if (prev <= 1) { clearInterval(timerRef.current) // 时间到自动切换下一题 handleNextQuestion() return 30 } return prev - 1 }) }, 1000) return () => clearInterval(timerRef.current) }, [currentQuestionIndex]) // 更新分数的回调,每个问题只允许加一次分 const handleAnswerSubmit = (isCorrect) => { if (isCorrect) { setScore(prev => prev + 1) } } // 切换下一题 function handleNextQuestion() { if (currentQuestionIndex < questions.length - 1) { setCurrentQuestionIndex(prev => prev + 1) } else { // 所有题目完成,可添加结束逻辑 alert(`答题结束!你的分数是:${score}/${questions.length}`) setCurrentQuestionIndex(0) setScore(0) } } return ( <div> <Header counter={counter} score={score} currentQuestion={currentQuestionIndex + 1} totalQuestions={questions.length} /> {questions.length > 0 && ( <Question key={questions[currentQuestionIndex].question} question={questions[currentQuestionIndex].question} answers={questions[currentQuestionIndex].shuffledAnswers} onAnswerSubmit={handleAnswerSubmit} /> )} <button onClick={handleNextQuestion} className='next-question-button'> {currentQuestionIndex < questions.length - 1 ? 'Next Question' : 'Restart'} </button> </div> ) } export default App
Question.js
import React from 'react' function Question(props) { const [selectedAnswer, setSelectedAnswer] = React.useState(null) const [answered, setAnswered] = React.useState(false) function handleAnswerClick(answer) { if (answered) return // 禁止重复点击 setSelectedAnswer(answer) setAnswered(true) props.onAnswerSubmit(answer.isCorrect) } // 解码HTML实体 function decodeHtml(html) { const txt = document.createElement("textarea") txt.innerHTML = html return txt.value } return ( <div className="question-container"> <h2 className="question">{decodeHtml(props.question)}</h2> <div className="answers-container"> {props.answers.map((answer, index) => { let buttonClass = "answer-button" if (answered) { if (answer.isCorrect) { buttonClass += " green-button" } else if (selectedAnswer === answer) { buttonClass += " red-button" } } return ( <button key={index} onClick={() => handleAnswerClick(answer)} className={buttonClass} disabled={answered} > {decodeHtml(answer.text)} </button> ) })} </div> </div> ) } export default Question;
Header.js
import React from 'react'; function Header(props) { return ( <div className="header"> <h1 className="trivia">Trivia</h1> <h2 className="header-time">{props.counter}</h2> <div className='score'> <h3 className="header-text header-score"> score: {props.score} | #{props.currentQuestion}/{props.totalQuestions} </h3> <h3 className="header-text">easy</h3> </div> </div> ) } export default Header;
style.css(保持原代码不变)
body { margin: 0; padding: 0; background-color: #F5F7FB; display: flex; flex-direction: column; justify-content: center; align-items: center; } .header { width: 80vw; height: 17vh; background-color: rgb(122, 132, 186); position: absolute; top: 5%; left: 10%; border-radius: 30px; display: flex; flex-direction: row; justify-content: space-around; align-items: center; } .header-button { width: 100px; height: 30px; border-radius: 10px; font-size: large; } .question-container { background-color: rgb(245, 188, 81); width: 80vw; height: 65vh; border-radius: 30px; display: flex; flex-direction: column; justify-content: center; align-items: center; position: absolute; top: 56%; left: 50%; transform: translate(-50%, -50%); } .question { margin: 40px; padding: 20px; justify-self: center; } .trivia { font-size: 30px; color: antiquewhite; } .score { display: flex; background-color: antiquewhite; border-radius: 10px;; } .header-text { margin: 10px; } .header-time { color: antiquewhite; } h1 { margin: 20px; } hr { width: 80vw; background-color: #4D5B9E; } .answer-button { background-color: azure; color:gray; width: 150px; height: 40px; margin: 20px; border: 0.794239px solid #4D5B9E; border-radius: 7.94239px; } .next-question-button { width: 80vw; height: 50px; border-radius: 20px; position: absolute; bottom: 5%; left: 10%; background-color: rgb(122, 132, 186); } @media only screen and (min-width: 480px) { .answers-container { display: flex; flex-direction: column; } } .green-button { background-color: rgb(37, 206, 37); color: black; } .red-button { background-color: rgb(240, 80, 80); color: black; }
内容的提问来源于stack exchange,提问作者mateuszdo
相关产品推荐
相关产品推荐

