【问题标题】:React - Component is rendered unnecessarily when state is changedReact - 组件在状态更改时不必要地呈现
【发布时间】:2020-11-20 02:36:53
【问题描述】:

我使用 React 函数组件开发了一个时钟组件。因为我需要每秒刷新时钟。我用过 setTimeout

我已使用“useState”将 setTimeout Id 存储为状态变量,稍后在卸载组件时用于清除计时器。
我在这里使用了状态而不是普通变量,因为后者将在每次组件呈现并且不维持最后分配的值时被初始化。

但令我沮丧的是,每次更新timeElement时,组件都会渲染两次,后来我发现当setTimeout Id在本地状态下更新时,组件又被渲染了一次.

您能告诉我如何避免重新渲染吗?

工作演示可用here

编辑: 许多人通过使用 setInterval 而不是 setTimeout 提供了解决方案。这很好,但仍然无法使用 setTimeout 本身来实现它吗?我们是否必须采用这种解决方案?

function ClockContainer() {
  let [color, setColor] = React.useState(null);
  let [show, setShow] = React.useState(true);
  let displayBtnClickHandler = function () {
    setShow(!show);
  };
  let onInputChangeHandler = function (orgEvent) {
    setColor(orgEvent.target.value);
  };
  return (
    <React.Fragment>
      <button onClick={displayBtnClickHandler}>
        {show ? "Hide" : "Show"}{" "}
      </button>
      {
       show && <input
          type="text"
          placeholder="Enter Color"
          onChange={onInputChangeHandler}
        /> 
      }
      
      {show ? <Clock color={color} /> : <h3>Clock Hidden!!</h3>}
      </React.Fragment>
  );
}

function Clock(props) {
  let getTimer = function () {
    let currentTime = new Date();
    let hours = currentTime.getHours();
    return {
      currentTime,
      hours,
      minutes: currentTime.getMinutes(),
      seconds: currentTime.getSeconds(),
      ampm: hours >= 12 ? "pm" : "am"
    };
  };
  let [timeElement, setTimeElements] = React.useState(getTimer());
  let [updateTimer, setUpdateTimer] = React.useState(false);

  let setTimer = function () {
    clearTimeout(updateTimer);
    let updateTimerlocal = setTimeout(() => {
      setTimeElements(getTimer());
    }, 1000);
    setUpdateTimer(updateTimerlocal);
  };

  React.useEffect(() => {
    console.log("Rendered");
  });

  React.useEffect(() => {
    setTimer();
  }, [timeElement]);
  /* To call cleanup codes when destroyed useEffect with empty array needs to be passed */
  React.useEffect(() => {
    return function () {
      console.log("UnMounted!!");
      clearTimeout(setTimer);
    };
  }, []);

  return (
      <div className="clock" style={{ backgroundColor: props.color }}>
        {timeElement.hours === 0
          ? 12
          : timeElement.hours > 12
          ? timeElement.hours - 12
          : timeElement.hours}
        :
        {timeElement.minutes > 9
          ? timeElement.minutes
          : `0${timeElement.minutes}`}
        :
        {timeElement.seconds > 9
          ? timeElement.seconds
          : `0${timeElement.seconds}`}{" "}
        {timeElement.ampm}
      </div>
  );
}

ReactDOM.render(<ClockContainer />, document.getElementById("app"));
:root {
  --firstColor: rgba(0, 169, 158, 1);
  --secondColor: rgba(0, 191, 150, 1);
  --textColor: #fff;
}

.clock {
  position: relative;
  border-radius: 0.5em;
  margin: 10px auto;
  font-family: "Open Sans", sans-serif;
  text-align: center;
  font-size: 29px;
  color: var(--textColor);
  line-height: 2em;
  background: var(--firstColor);
  width: fit-content;
  padding: 10px 20px;
}

@media only screen and (min-width: 700px) and (max-width: 1000px) {
  .clock {
    font-size: 50px;
  }
}

@media only screen and (min-width: 500px) and (max-width: 699px) {
  .clock {
    font-size: 45px;
  }
}

@media only screen and (min-width: 200px) and (max-width: 499px) {
  .clock {
    font-size: 40px;
  }
}
<div id=app></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>

【问题讨论】:

    标签: javascript html reactjs


    【解决方案1】:

    您正在通过调用 setTimer() 的 useEffect 循环更新 timeElement 来进行重新渲染循环。

    要解决这个问题,只需调用一次setTimer()

    React.useEffect(() => {
       setTimer();
    }, []);
    

    并将 setTimeout 交换为 setInterval。请注意 - 重新检查 clearTimeouts 现在是否被正确调用并且没有引入内存泄漏。

    See updated example

    【讨论】:

    • 感谢您的解决方案@Soapnrope。但即使在这种情况下,第一次,组件也会被渲染两次。你能找到避免它的解决方案吗?
    • @rachCoder27 我再次更新了示例codepen。本质上,我删除了 setTimer 并使其成为一个简单的 useEffect 来处理clearTimeout。我还删除了 getTimer 以在组件外部运行,否则如果您在组件范围内定义函数,它们将在每次渲染时重新分配。在这种情况下,它不需要。
    【解决方案2】:

    问题是 2 个使用状态 setTimeElementssetUpdateTimer。两者都被调用并且它们中的每一个都导致重新渲染。

    setUpdateTimer 在这里似乎没有必要,因为它与 UI 更新无关。它可以是普通的 javascript 变量来存储 id。

    这是固定位。变量updateTimerlocal 已被移出。在分配新值之前,请清除间隔以避免多个间隔。

      let [timeElement, setTimeElements] = React.useState(getTimer());
      let updateTimerlocal = null;
      let setTimer = function () {
        if (updateTimerlocal) clearTimeout(updateTimer);
        updateTimerlocal = setTimeout(() => {
          setTimeElements(getTimer());
        }, 1000);
      };
    

    此外,对于此计时器用例,setInterval 将是一个更好的选择,而不是 setTimeout。

    希望对您有所帮助。如有任何疑问/澄清,请回复。

    【讨论】:

    • 我试过你的解决方案,当组件被卸载时,仍然没有清除计时器。 updateTimerlocal 未正确更新。你可以在这里查看 - codepen.io/rakshu/pen/JjGQZOP?editors=0010。即使通过单击隐藏按钮卸载它,计时器仍然运行。
    • 这是因为卸载组件时必须清除updateTimerlocal(使用clearInterval)。对于setInterval,我们使用clearInterval。在 useEffect 钩子中返回的函数中执行 clearInterval(与 setTimer 的 clearTimeout 一起),它应该可以正常工作
    • 您好,已更新。但问题仍然是一样的。没关系,我明白了“updateTimerlocal”声明必须移到使用它的 useEffect 挂钩中。 @Sunil
    【解决方案3】:

    问题是这个函数在这里你已经定义了你的逻辑并且你正在设置你的状态是 setTimeElements(getTimer());

     let setTimer = function () {
        clearTimeout(updateTimer);
        let updateTimerlocal = setTimeout(() => {
          setTimeElements(getTimer());
        }, 1000);
        setUpdateTimer(updateTimerlocal);
      };
    

    你在 useFffect 中调用这个函数,你也有依赖 timeElement

     React.useEffect(() => {
            setTimer();
        }, [timeElement]);
    

    所以当你的状态 timeElement 发生变化时,你的 useEffect 会被再次调用

    所以最好像@Soapnrope 提到的那样删除依赖,这样它就会被调用一次

     React.useEffect(() => {
        setTimer();
      }, []);
    

    【讨论】:

      【解决方案4】:

      React 组件会在每次状态更改时重新渲染。如果你想在组件中持久化一个值而不必重新渲染它以改变值,你可以使用 useRef。在 useEffect 中始终具有依赖函数 (setTimer)。这是 React 推荐的解决方案。我已在link 中提供了解决方案。

        const timerRef = React.useRef(null);
        React.useEffect(() => {
          const setTimer = function () {
            clearTimeout(timerRef.current);
            timerRef.current = setTimeout(() => {
              setTimeElements(getTimer());
            }, 1000);
          };
          setTimer();
          return function () {
            // console.log("UnMounted!!");
            clearTimeout(timerRef.current);
          };
        }, [timeElement]);
      

      【讨论】:

      • 感谢@Aishwarya 提供的解决方案。我开始了解“useRef”。但是在这个用例中,我们可以将它作为局部变量而不是“useRef”。你和 Soapnrope 的解决方案结合起来,我可以得出这个解决方案 - codepen.io/rakshu/pen/rNxEERM?editors=0011。谢谢你:)
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2019-12-04
      • 2021-12-21
      • 2016-12-05
      • 2021-07-10
      • 2019-04-10
      • 2021-01-12
      • 1970-01-01
      相关资源
      最近更新 更多