简体   繁体   English

状态在 useState 的函数之间不持久

[英]State not persisting between functions with useState

I am practicing React useState hooks to make a quiz with a ten second timer per question.我正在练习使用 React useState hooks 来制作每个问题 10 秒计时器的测验。

Currently I am able to get the quiz questions from an API, set them to state, and render them.目前我能够从 API 获取测验问题,将它们设置为状态,并呈现它们。 If a user clicks an answer, the question is removed from the array in state, the seconds in state is reset to 10 and the next question renders.如果用户单击答案,则问题将从状态数组中删除,状态秒数重置为 10 并呈现下一个问题。

I am trying to get the timer to clear when there is nothing left in the array of questions in state.当状态问题数组中没有任何内容时,我试图让计时器清除。 When I console.log(questions) in the startTimer function, it is an empty array, despite the same console.log showing the data in the userAnswer function?当我在 startTimer 函数中使用 console.log(questions) 时,它是一个空数组,尽管相同的 console.log 显示了 userAnswer 函数中的数据? What am I missing here?我在这里缺少什么?

I've removed the referenced shuffle function to save space我已经删除了引用的 shuffle 函数以节省空间

function App() {

  // State for trivia questions, time left, right/wrong count
  const [questions, setQuestions] = useState([])
  const [seconds, setSeconds] = useState(10);
  const [correct, setCorrect] = useState(0);
  const [incorrect, setIncorrect] = useState(0);

  // Get data, set questions to state, start timer
  const getQuestions = async () => {
    let trivia = await axios.get("https://opentdb.com/api.php?amount=10&category=9&difficulty=easy&type=multiple")
    trivia = trivia.data.results
    trivia.forEach(result => {
      result.answers = shuffle([...result.incorrect_answers, result.correct_answer])
    })
    setQuestions(trivia)
    startTimer()
  }

  const startTimer = () => {

    // Empty array here, but data at beginning of userAnswer
    console.log(questions)

    const interval = setInterval(() => {

      // If less than 1 second, increment incorrect, splice current question from state, clear interval and start timer back at 10
      setSeconds(time => {
        if (time < 1) {
          setIncorrect(wrong => wrong + 1)
          setQuestions(q => {
            q.splice(0,1)
            return q;
          })
          clearInterval(interval);
          startTimer()
          return 10;
        }
        // I want this to hold the question data, but it is not
        if (questions.length === 0) {
          console.log("test")
        }
        // Else decrement seconds 
        return time - 1;
      });
    }, 1000);
  }

  // If answer is right, increment correct, else increment incorrect
  // Splice current question from const questions
  const userAnswer = (index) => {

    // Shows array of questions here
    console.log(questions)

    if (questions[0].answers[index] === questions[0].correct_answer) {
      setCorrect(correct => correct + 1)
    } else {
      setIncorrect(incorrect => incorrect + 1)
    }
    setQuestions(q => {
      q.splice(0,1);
      return q;
    })
    
    // Shows same array here despite splice in setQuestions above
    console.log(questions)
    setSeconds(10);
  }

  return (
    <div>
      <button onClick={() => getQuestions()}></button>
      {questions.length > 0 &&
        <div>
          <Question question={questions[0].question} />
          {questions[0].answers.map((answer, index) => (
            <Answer
              key={index}
              answer={answer}
              onClick={() => userAnswer(index)} />
          ))}
        </div>
      }
      <p>{seconds}</p>
      <p>Correct: {correct}</p>
      <p>Incorrect: {incorrect}</p>
    </div>

  )
}

export default App;

Each render of your App will create new bindings of its inner functions and variables (like questions and startTimer ). App每次渲染都会为其内部函数和变量(如questionsstartTimer )创建新的绑定。

When the button is clicked and getQuestions runs, getQuestions then calls the startTimer function.当点击该按钮getQuestions运行, getQuestions然后调用startTimer功能。 At that point in time, the startTimer function closes over the questions variable that was defined at the time the button was clicked - but it's empty at that point.在那个时间点, startTimer函数关闭在单击按钮时定义的questions变量 -但那时它是空的 Inside the interval, although the function has re-rendered, the interval callback is still referencing the questions in the old closure.在区间内部,虽然函数重新渲染了,区间回调仍然引用旧闭包中的questions

Rather than depending on old closures, I'd initiate a setTimeout on render (inside a useLayoutEffect so the timeout can be cleared if needed) if the questions array is populated.如果填充了questions数组,我将在渲染时启动setTimeout (在useLayoutEffect以便在需要时可以清除超时),而不是依赖于旧的闭包。

Another issue is that splice should not be used with React, since that mutates the existing array - use non-mutating methods instead, like slice .另一个问题是splice不应该与 React 一起使用,因为它会改变现有数组 - 改用非变异方法,例如slice

Try something like the following:尝试类似以下内容:

// Get data, set questions to state, start timer
const getQuestions = async () => {
  let trivia = await axios.get("https://opentdb.com/api.php?amount=10&category=9&difficulty=easy&type=multiple")
  trivia = trivia.data.results
  trivia.forEach(result => {
    result.answers = shuffle([...result.incorrect_answers, result.correct_answer])
  })
  setQuestions(trivia);
  setSeconds(10);
}
useLayoutEffect(() => {
  if (!questions.length) return;
  // Run "timer" if the quiz is active:
  const timeoutId = setTimeout(() => {
    setSeconds(time => time - 1);
  }, 1000);
  return () => clearTimeout(timeoutId);
});
// If quiz is active and time has run out, mark this question as wrong
// and go to next question:
if (questions.length && time < 1) {
  setIncorrect(wrong => wrong + 1)
  setQuestions(q => q.slice(1));
  setSeconds(10);
}
if ((correct || incorrect) && !questions.length) {
  // Quiz done
  console.log("test")
}

This is because of closure .这是因为关闭

When getQuestions executes this part:getQuestions执行这部分时:

    setQuestions(trivia)
    startTimer()

Both functions run in the same render cycle.这两个函数在同一个渲染周期中运行。 In this cycle questions is still [] (or whatever previous value it held).在这个循环中, questions仍然是[] (或者它之前持有的任何值)。 So when you call console.log(questions) inside startTimer , you get an empty array.因此,当您在startTimer调用console.log(questions) ,您会得到一个空数组。

You can fix that by invoking startTimer in the next render cycle with useEffect您可以通过在下一个渲染周期中使用useEffect调用startTimer来解决这个useEffect

  const [ start, setStart ] = useState(false)
 
  const getQuestions = async () => {
    let trivia = await axios.get("https://opentdb.com/api.php?amount=10&category=9&difficulty=easy&type=multiple")
    trivia = trivia.data.results
    trivia.forEach(result => {
      result.answers = shuffle([...result.incorrect_answers, result.correct_answer])
    })
    setQuestions(trivia)
    setStart(true)
  }

  useEffect(()=>{
   if (start) {
    startTimer();
    setStart(false)
   }
  },[start])

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM