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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.20 15:36:28