【问题标题】:Timer with react hooks doesn't update after re-rendering重新渲染后带有反应钩子的计时器不会更新
【发布时间】:2020-08-18 15:58:36
【问题描述】:

我正在使用 React 钩子创建一个计时器。这是一个用于测验的简单组件。每个问题都有一个定义的持续时间,因此计时器应该从这个持续时间开始,并开始减少一秒。我的问题是组件做了应该做的事情,但是当我进入下一个问题时,状态不会初始化为道具中传递的持续时间,而是继续使用计数器......

  const Timer = ({ duration }) => {
  const [counter, setCounter] = useState(duration);
  const timer = useRef();
  debugger;
  console.log("counter: " + counter);
  const setTimer = () => {
    if (counter <= duration) {
      setCounter(counter - 1);
    }
  };
  // Similar to componentDidMount and componentDidUpdate:
  useEffect(() => {
    timer.current = setTimeout(() => setTimer(), 1000);
    return () => {
      if (timer.current) {
        console.log("ClearInterval in Timer");
        clearTimeout(timer.current);
      }
    };
  }, [counter]);
  return (
    <div>
      <p>time left {counter} seconds</p>
    </div>
  );
};

export default Timer;

我正在从 Card 组件渲染 Timer 组件:

import React from "react";
import QuizButton from "../QuizButton/QuizButton";
import Timer from "../Timer/Timer";
import "./styles.css";

const QuizQuestion = ({ question, responses, checkAnswerFn, duration }) => (
  <article className='card'>
    <header>
      <Timer duration={duration} />
    </header>
    <div>
      <p>{question}</p>
    </div>
    <footer>
      {responses.map((response, i) => {
        return (
          <QuizButton key={i} onClick={checkAnswerFn(response)}>
            {response}
          </QuizButton>
        );
      })}
    </footer>
  </article>
);

export default QuizQuestion;

有谁知道为什么在问题重新呈现后状态没有初始化为持续时间?

【问题讨论】:

  • State 不应该为后续渲染重新初始化,这就是重点。你的意思是组件Timer应该重新挂载吗?
  • 不,我希望下次渲染Timer,因为渲染了下一个Question,持续时间从props中传递的值开始
  • 我真的不认为你这样做......如果每次Timer 呈现时counter 都重置为duration,它永远不会减少。调用 setCounter 会触发 Timer 的重新渲染。
  • 如果你不想让它重新挂载,而只是在duration改变时重置counter,那么也许你可以设置一个useEffectduration属性改变时运行?但是,如果 duration 值没有改变每个问题,那么这也不起作用。
  • 谢谢布赖恩,你是对的,我每次使用 setCounter 时都会重新渲染组件。我以为一旦我用不同的 props 值再次重新渲染 Timer,那么 useState 会将状态重新初始化为 props 中传递的值......但我想我并不完全了解组件的生命周期......跨度>

标签: reactjs react-hooks


【解决方案1】:

看起来有点复杂,以驱动您的代码如何以及为什么无法按预期工作,但这里有一个今天刚刚完成的工作示例,希望它有助于为您指明正确的方向:

const [counter, setCounter] = useState(15000);

...


useEffect(() => {
    const myInterval = () => {
      if (counter > 1000) {
        setCounter(state => state - 1000)
      } else if (counter !== 0){
        setCounter(0);
        clearInterval(interval);
      }
    }
    const interval = setInterval(myInterval, 1000);
    return () => {
      clearInterval(interval)
    }
  }, [counter]);

...

console.log(counter); // 15000, 14000, 13000, ...

【讨论】:

  • 您好阿道夫,感谢您的回复。正如我在下面向 Fauzul 提到的那样,我的问题是每个问题都有自己的持续时间。我渲染 Timer 组件,从 Question 组件中传递道具持续时间。我遇到的问题是,当我回答第一个问题时,第二个问题的计时器不会在持续时间重新启动......而是上一个问题的计数器。
  • 好的!那么,如果您将duration 属性添加到useEffect 依赖项[counter, duration] 中,这样您就可以在每次持续时间属性更改时setCounter ;-)
  • 嗨,阿道夫,你指出了我正确的方向,但这只适用于每次持续时间不同的情况。如果我有 2 个持续时间相同的问题,则组件不会重新启动...
【解决方案2】:

setTimer 函数中的条件不允许 counter 初始化。让您的函数递减计数器,直到并且除非计数器值大于 0,否则使用 duration 对其进行初始化。将您的函数替换为以下内容,它将帮助您将计时器初始化回持续时间。

const setTimer = () => {
  setCounter(counter > 0  ? counter - 1 : duration);
};

【讨论】:

    【解决方案3】:

    更新 2 - 添加了问题数组和基于问题的计时器数据

    更新 1 - 我认为它现在可以如您所愿

    总结 - 我提升了一些状态。

    所以编辑后的答案是:由于我们需要将新的道具数据重新发送到 Child Timer 组件,而触发器是基于我使用子 Timer 组件的单击演示的子组件,所以我们从父 Question 组件到子组件,子组件将通过 prop 在 onClick 侦听器上调用。因此,该函数更新父级 (Question.js) 的状态,重新渲染子级 (Timer.js) 并传递我们设置为新的新属性值。我们没有中继基于计时器的更改,而是更改为基于数据的触发器,例如 Question Data 更改为调用 useEffect。

    我希望解释有效并解决您的用例。

    ..

    //Question.js    
    
    import React, { useState, useEffect } from "react";
    import Timer from "./Timer";
    import "./styles.css";
    
    const Question = () => {
      const questionPool = [
        { number: 1, text: "lorem", timer: 6 },
        { number: 2, text: "lorem", timer: 10 },
        { number: 3, text: "lorem", timer: 20 },
        { number: 4, text: "lorem", timer: 3 },
        { number: 5, text: "lorem", timer: 12 }
      ];
      const [countDown, setCountDown] = useState(questionPool[0].timer);
      const [currentQuestion, setCurrentQuestion] = useState(
        questionPool[0].number
      );
    
    
      const resetCount = (newQuestion) => {
        newQuestion < questionPool.length + 1
          ? setCurrentQuestion(newQuestion)
          : setCurrentQuestion(1);
      };
    
      useEffect(() => {
        if (questionPool[currentQuestion]) {
          setCountDown(questionPool[currentQuestion].timer);
        } else {
          setCountDown(questionPool[0].timer);
        }
      }, [currentQuestion]);
    
      return (
        <>
          <Timer
            duration={countDown}
            resetCountDown={resetCount}
            questionNumber={currentQuestion}
          />
        </>
      );
    };
    
    export default Question;
    

    ....

    //Timer.js
    
    import React, { useEffect, useState } from "react";
    import "./styles.css";
    
    const Timer = ({ duration, resetCountDown, questionNumber}) => {
      const [counter, setCounter] = useState(duration);
      let interval = null;
      useEffect(() => {
        const myInterval = () => {
          if (counter > 1) {
            setCounter((state) => state - 1);
          } else if (counter !== 0) {
            setCounter(0);
            clearInterval(interval);
          }
        };
        interval = setInterval(myInterval, 1000);
        return () => {
          clearInterval(interval);
        };
      }, [counter]);
    
      useEffect(() => {
        if (!interval) {
          setCounter(duration);
        }
      }, [questionNumber]);
    
      return (
        <div>
          <p>
            time left {counter} seconds For Question: {questionNumber}
          </p>
          <button type="button" onClick={() => resetCountDown(questionNumber + 1)}>
            next
          </button>
        </div>
      );
    };
    
    export default Timer;
    

    CodeSandbox Extending from @Adolfo Onrubia to match your use case

    检查这是否有帮助。

    ********* 以下不推荐使用 *******************

    如果您仍想动态添加计时器组件,我认为您需要在道具中推入整个反应组件,并让正在推新组件的组件重新渲染,以便孩子将自己渲染和一个新问题可以出现一个新的组件。

    总之,

    • 您的 QuestionsArray[{OBJECT WITH QUESTION DETAILS}] 在您的主应用中。
    • 您的 useState 如下:

    const [currentQuestion, setCurrentQuestion] = useState(QuestionArray[0]);

    每当按下下一个按钮时。你会的

    setCurrentQeustion(QuestionArray[NewIndex]);

    因此,您的主组件 App 组件的状态会在 currentQuestionuseEffect 上重新呈现,并将其传递给 Timer.js/child 组件,导致它使用新的 Timeout 重新呈现持续时间。

    检查 props.children 也可能是值得的,所以不要像模板一样将其作为常规道具发送。

    <Template>
     {currentQuestion}
    </Template>
    

    【讨论】:

    • 感谢 Fauzul,我添加了渲染 Timer 组件的方式。如果我正确理解了我所做的方式,它将不会提供预期的输出(每次呈现新问题时,计时器将在问题持续时间开始)...
    • @Javip 添加了具有自己的时间计数器持续时间数据的问题数组。
    • 感谢 Fauzul,通过您的回复和下面 Adolfo Onrubia 的回复,我设法解决了这个问题。正如您所提到的,我解决了它,将问题的 id 传递给 Timer 并在其上使用 useEffect。由于 id 总是不同的,因此计时器会在 id 更改时更新持续时间......我在下面发布了我的代码
    【解决方案4】:

    感谢 Fauzul 和 Adolfo,我终于解决了我的问题。

    我将问题的 id 传递给计时器的道具,并使用 useEffect 挂钩,每次 id 更改时,计数器都会更新为持续时间...

    QuizQuestion.js

    import React from "react";
    import PropTypes from "prop-types";
    import QuizButton from "../QuizButton/QuizButton";
    import Timer from "../Timer/Timer";
    import "./styles.css";
    
    const QuizQuestion = ({ question, responses, checkAnswerFn, duration, id }) => (
      <article className='card'>
        <header>
          <Timer duration={duration} id={id} />
        </header>
        <div>
          <p>{question}</p>
        </div>
        <footer>
          {responses.map((response, i) => {
            return (
              <QuizButton key={i} onClick={checkAnswerFn(response)}>
                {response}
              </QuizButton>
            );
          })}
        </footer>
      </article>
    );
    
    QuizQuestion.propTypes = {
      question: PropTypes.string.isRequired,
      responses: PropTypes.array.isRequired,
      checkAnswerFn: PropTypes.func.isRequired,
    };
    
    export default QuizQuestion;
    

    Timer.js

    import React, { useState, useEffect, useRef } from "react";
    import PropTypes from "prop-types";
    
    const Timer = ({ duration, id }) => {
      const [counter, setCounter] = useState(duration);
      const timer = useRef();
      const setTimer = () => {
        if (counter > 0) {
          setCounter(counter - 1);
        }
      };
      // Similar to componentDidMount and componentDidUpdate:
      useEffect(() => {
        timer.current = setTimeout(() => setTimer(), 1000);
        return () => {
          if (timer.current) {
            clearTimeout(timer.current);
          }
        };
      }, [counter]);
    
      useEffect(() => {
        setCounter(duration);
      }, [id]);
    
      return (
        <div>
          <p>time left {counter} seconds</p>
        </div>
      );
    };
    
    Timer.propTypes = {
      duration: PropTypes.number.isRequired,
      id: PropTypes.number.isRequired,
    };
    
    export default Timer;
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2021-11-10
      • 1970-01-01
      • 2020-05-01
      • 1970-01-01
      • 2019-08-30
      • 2020-07-20
      • 1970-01-01
      相关资源
      最近更新 更多