如何在 React 功能组件中正确设置 setInterval 计时器?

问题描述 投票:0回答:6

我刚刚开始学习 React,我正在观看一个涉及状态和钩子的教程。它只是每 1000 毫秒处理一次更新时间(或者我是这么认为的)。

import React from "react";
let count = 0;

function App() {
  const now = new Date().toLocaleTimeString();
  let [time, setTime] = React.useState(now);


  function updateTime(){
    const newTime = new Date().toLocaleTimeString();
    setTime(newTime);
    count++;
    console.log(count);
    console.log(new Date().getMilliseconds());
  }

  setInterval(updateTime, 1000);


  return (
    <div className="container">
      <h1>{time}</h1>
      <button onClick = {updateTime}>time</button>
    </div>
  );
}

export default App;

本教程的目的只是一个关于如何更新时间的简单示例,但我注意到它每 1000 毫秒更新多次(突发)。我怀疑每次对钩子的更改都会呈现新组件,但旧组件仍然在那里更新并生成更多组件,导致每 1000 毫秒的调用似乎呈指数级增长。

我很好奇这里发生了什么?假设有一个每 1000 毫秒更新一次的简单计数器,我该怎么做?

setTime(count)
显然行不通

javascript reactjs react-component
6个回答
17
投票

问题:在您当前的实现中,每次组件渲染时都会调用

setInterval
(即,在设置时间状态后也将调用),并且将创建一个新的间隔 - 这产生了这种“指数增长”如您的控制台中所示。

正如评论部分所解释的:在 React 中处理函数组件时,

useEffect
将是处理这种情况的最佳方法。看看我下面的例子。
useEffect
这里只会在初始组件渲染后(当组件安装时)运行。

React.useEffect(() => {
  console.log(`initializing interval`);
  const interval = setInterval(() => {
    updateTime();
  }, 1000);

  return () => {
    console.log(`clearing interval`);
    clearInterval(interval);
  };
}, []); // has no dependency - this will be called on-component-mount

如果您想运行效果并仅清理一次(在安装和 unmount),您可以传递一个空数组([])作为第二个参数。这 告诉 React 你的效果不依赖于 props 中的任何值 或状态,所以它永远不需要重新运行。

在您的场景中,这是“空数组作为第二个参数”的完美用法,因为您只需要在安装组件时设置间隔并在卸载时清除间隔。看看

useEffect
返回的函数。这是我们的清理函数,它将在组件卸载时运行。这将“清理”,或者在这种情况下,当组件不再使用时清除间隔。

我编写了一个小应用程序,演示了我在这个答案中涵盖的所有内容:https://codesandbox.io/s/so-react-useeffect-component-clean-up-rgxm0?file=/src/App .js

我合并了一个小型路由功能,以便可以观察到组件的“卸载”。


我的旧答案(不推荐):

每次组件重新渲染时都会创建一个新的间隔,这就是您为时间设置新状态时发生的情况。我要做的是在设置新的间隔之前清除之前的间隔(

clearInterval

try {
  clearInterval(window.interval)
} catch (e) {
  console.log(`interval not initialized yet`);
}

window.interval = setInterval(updateTime, 1000);

https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setInterval


4
投票

Macro Amorim 回答说,

useEffect
是做到这一点的最佳方法。代码在这里:

useEffect(() => {
    const interval = setInterval(() => {
         const newTime = new Date().toLocaleTimeString();
         setTime(newTime);
    }, 1000)

    return () => {
        clearInterval(interval);
    }
}, [time])

3
投票

你也可以试试这个 -

import React, { useEffect, useState } from 'react';

export default function App() {
  const [timer, setTimer] = useState(0);
  const [toggle, setToggle] = useState(false);

  useEffect(() => {
    let counter;
    if (toggle) {
      counter = setInterval(() => setTimer(timer => timer + 1), 1000);
    }
    return () => {
      clearInterval(counter);
    };
  }, [toggle]);

  const handleStart = () => {
    setToggle(true);
  };

  const handleStop = () => {
    setToggle(false);
  };

  const handleReset = () => {
    setTimer(0);
    setToggle(false);
  };

  return (
    <div>
      <h1>Hello StackBlitz!</h1>
      <p>Current timer - {timer}</p>
      <br />
      <button onClick={handleStart}>Start</button>
      <button onClick={handleReset}>Reset</button>
      <button onClick={handleStop}>Stop</button>
    </div>
  );
}

0
投票

在这种情况下,你可以使用 React 中的 useEffect 钩子,在函数内部返回时,你可以使用clearInterval 函数,我建议你查一下,useEffect 非常适合你想要做的事情。


0
投票

使用 Hooks 的简单定时器

import React, { useEffect, useState } from "react";

const TimeHeader = () => {
  const [timer, setTimer] = useState(new Date());

  useEffect(() => {
    const interval = setInterval(() => {
      setTimer(new Date());
    }, 1000);
  }, []);

  return (
    <div className="electonTimer">
      <div className="electionDate">
        <h1>{timer.toLocaleTimeString()}</h1>
      </div>
    </div>
  );
};


0
投票

以上答案均不能满足我的要求。如果我想从父组件传递计时器怎么办?如果倒计时器值是一个状态怎么办?接受的答案在渲染后立即开始间隔,这在许多情况下可能会很糟糕!该倒计时组件的用户必须卸载倒计时组件才能运行清理代码,这意味着如果我们从未卸载该组件(通过条件渲染或其他方式),我们可能永远不会最终调用清理代码,这是一个很大的缺陷。

我对这些问题的解决办法如下:

import React, { useEffect, useRef } from "react";

export type Props = {
  seconds: number;
  decreaseCountDown: () => void;
};

export const MIN_TIMER_VALUE = -1;

const CountDownTimer = ({ seconds, decreaseCountDown }: Props) => {
  const intervalId = useRef<NodeJS.Timer | undefined>(undefined);

  useEffect(() => {
    createIntervalIfRequired();
    return () => clearIntervalIfRequired();
  }, [seconds]);

  const createIntervalIfRequired = () => {
    if (intervalId.current !== undefined) {
      return;
    }
    if (seconds <= MIN_TIMER_VALUE) {
      return;
    }
    const createdIntervalId = setInterval(() => {
      decreaseCountDown();
    }, 1000);

    intervalId.current = createdIntervalId;
  };
  
  const clearIntervalIfRequired = () => {
    if (seconds > MIN_TIMER_VALUE || intervalId.current == undefined) {
      return;
    }
    clearInterval(intervalId.current);
    intervalId.current = undefined;
  };

  return (
    <span className={`px-2 ${seconds <= MIN_TIMER_VALUE ? "hidden" : ""}`}>
      {beautifyTime(seconds)}
    </span>
  );
};

const beautifyTime = (time: number): string => {
  let minutes = parseInt((time / 60).toString()).toString();
  let seconds = parseInt((time % 60).toString()).toString();

  if (seconds.length == 1) {
    seconds = "0" + seconds;
  }

  if (minutes.length == 1) {
    minutes = "0" + minutes;
  }

  return `${minutes}:${seconds}`;
};

export default CountDownTimer;

这样,只有当 props

seconds
大于 0 时,我们才会创建间隔,并且即使我们的组件没有卸载,我们也会清理间隔! 如果我们现在没有任何间隔旋转,我们只会开始一个新的间隔。

该组件的使用者将执行以下操作:

<CountDownTimer seconds={countDownTimer} decreaseCountDown={() => { setCountDownTimer((currentCounter) => currentCounter - 1); }} />
这样

消费者不关心拆卸组件或创建多少个间隔。它只知道当前的 seconds

 值是多少。

**** 即使在react.dev 文档上,他们也使用

useRef

 钩子来表示intervalId。**

© www.soinside.com 2019 - 2024. All rights reserved.