【问题标题】:State not persisting between functions with useState状态不在使用 useState 的函数之间保持不变
【发布时间】:2020-12-15 20:34:49
【问题描述】:

我正在练习 React useState hooks 来做一个每个问题有 10 秒计时器的测验。

目前,我能够从 API 获取测验问题,将它们设置为状态,然后呈现它们。如果用户点击一个答案,则问题会从状态数组中移除,状态秒数重置为 10 并呈现下一个问题。

当状态中的问题数组中没有任何内容时,我正在尝试清除计时器。当我在 startTimer 函数中使用 console.log(questions) 时,它是一个空数组,尽管相同的 console.log 在 userAnswer 函数中显示数据?我在这里错过了什么?

我已经删除了引用的随机播放函数以节省空间

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;

【问题讨论】:

    标签: javascript reactjs use-state


    【解决方案1】:

    App 的每次渲染都会为其内部函数和变量创建新的绑定(例如 questionsstartTimer)。

    当单击按钮并运行getQuestions 时,getQuestions 然后调用startTimer 函数。在那个时间点,startTimer 函数关闭了在单击按钮时定义的questions 变量 - 但在那个时候它是空的。在区间内,虽然函数已经重新渲染,但区间回调仍然引用了旧闭包中的questions

    如果填充了questions 数组,我会在渲染时启动setTimeout(在useLayoutEffect 内,以便可以在需要时清除超时),而不是依赖旧的闭包。

    另一个问题是 splice 不应该与 React 一起使用,因为它变异现有数组 - 使用非变异方法代替,例如 slice

    尝试以下方法:

    // 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")
    }
    

    【讨论】:

    • 感谢您的回复。由于更熟悉 useEffect,我选择了其他解决方案,但我肯定会很快研究 useLayoutEffect。
    • 你可以使用上面的useEffect而不是useLayoutEffect,我使用useLayoutEffect的唯一原因是它在渲染组件时会运行得更快一些; useEffect 仅在延迟后运行。因此,useLayoutEffect 将产生更准确的计时器。
    • 除了时间问题,在这种情况下,它们的行为是相同的。
    • 有趣。我也会尝试这个解决方案。再次感谢。
    • 它正在测试correctincorrect 中的至少一个是否非零,只有在测验开始时才会发生。如果您愿意,您也可以使用null 作为初始值,但从 0 开始会使事情更容易处理,从那时起,您可以在得分时添加一个。
    【解决方案2】:

    这是因为closure

    getQuestions执行这部分时:

        setQuestions(trivia)
        startTimer()
    

    这两个函数在同一个渲染周期中运行。在这个循环中,questions 仍然是 [](或者它之前持有的任何值)。因此,当您在 startTimer 中调用 console.log(questions) 时,您会得到一个空数组。

    您可以通过在下一个渲染周期中使用 useEffect 调用 startTimer 来解决此问题

      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])
    

    【讨论】:

    • 感谢您的回复和有用的文章。我已经阅读了有关 useEffect 的内容,因此我将采用此解决方案。
    • 酷,但@CertainPerformance 提出了一个关于setInterval 回调的重要观点,我忽略了这一点。在回调内部,questions 数组将始终具有 trivia 最初具有的任何值。这又是因为闭包的工作方式。不过,如果你只使用setQuestions 就可以了。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多