设置间隔不更新变量

set interval not updating the variable

我正在尝试捕获用户的点击次数。 我希望每 15 分钟发送一次 api, 我在 useEffect 中使用 setinterval 来实现这一点,但问题是即使状态在外部发生变化但在 setinterval 内部没有发生变化。 setinterval 仅给出初始默认值。

这是代码-

const Agent = (props: RouteComponentProps) => {
  const [clicks, setClicks] = useState(0);

  const handleOnIdle = () => {
    console.log("user is idle");
    console.log("last active", new Date(getLastActiveTime()));
    console.log("total idle time: ", getTotalIdleTime() / 1000);
  };

  const handleOnActive = () => {
    console.log("user is active");
    console.log("time remaining", getRemainingTime());
  };

  const handleOnAction = () => {
    console.log("user did something", clicks);
    setClicks(clicks + 1);
  };

  const {
    getRemainingTime,
    getLastActiveTime,
    getTotalIdleTime,
  } = useIdleTimer({
    timeout: 10000,
    onIdle: handleOnIdle,
    onActive: handleOnActive,
    onAction: handleOnAction,
    debounce: 500,
  });

  useEffect(() => {
    let timer = setInterval(
      () => alert(`user clicked ${clicks} times`),
      1000 * 30
    );

    return () => {
      clearInterval(timer);
      setClicks(0);
    };
  }, []);

  return (
    <AgentLayout>
      <div className="dashboard-wrapper py-3">
        <Switch>
          <Redirect
            exact
            from={`${props.match.url}/`}
            to={`${props.match.url}/home`}
          />
          <Route path={`${props.match.url}/home`} component={Home} />
          <Route
            path={`${props.match.url}/lead-details`}
            component={leadDetails}
          />
          <Route
            path={`${props.match.url}/fill-details`}
            component={FillDetails}
          />
          <Route
            path={`${props.match.url}/my-desktime`}
            component={MyDesktime}
          />

          <Redirect to="/error" />
        </Switch>
      </div>
    </AgentLayout>

提醒 用户点击了 0 次

问题是定时器函数只使用了 clicksfirst 版本,因为它关闭了组件函数第一次使用的版本叫做。当你将一个空的依赖数组传递给 useEffect 时,useEffect 回调只会被调用一次,即第一次调用组件函数时(当挂载组件时)。

您可以通过让定时器功能改用 setClicks 方法来解决这个问题:

useEffect(() => {
    let timer = setInterval(
        () => {
            setClicks(currentClicks => {                      // ***
                alert(`user clicked ${currentClicks} times`); // ***
                return currentClicks;                         // ***
            });                                               // ***
        },
        1000 * 30
    );

    return () => {
        clearInterval(timer);
        // setClicks(0); // <=== Don't do this, the component is unmounting
    };
}, []);

现在,计时器使用回调形式调用setClicks,这意味着它接收clicks的当前值。因为它 returns 相同的值,所以它不会更新组件的状态。

也可以通过添加 clicks 作为对 useEffect 的依赖来解决这个问题,但这有点复杂。天真的方法就是这样做:

// The naive way
useEffect(() => {
    let timer = setInterval(
        () => {
            alert(`user clicked ${clicks} times`);
        },
        1000 * 30
    );

    return () => {
        clearInterval(timer);
        // setClicks(0); // <=== Don't do this, the user's click count would get reset every time
    };
}, [clicks]);
//  ^^^^^^−−−−−−−−−−−−− ***

这大部分会起作用,但每次 clicks 更改时它都会重新启动计时器,即使这意味着等待 45 秒而不是等待 30 秒(因为 clicks 在 15 秒后更改,它取消了之前的间隔并再次开始)。对于非常短的间隔,这可能无关紧要,但对于 30 秒的间隔,它似乎不太理想。在不弄乱间隔时间的情况下这样做需要您记住下一次计时器回调应该发生的时间并调整初始延迟的持续时间以匹配,这变得相当复杂。


我之前没有注意到,但是任何时候你根据现有状态更新状态,我建议使用回调形式。所以你的

const handleOnAction = () => {
    setClicks(clicks => clicks + 1); // Note the function callback
};

问题

  1. 你有一个 clicks 状态的陈旧外壳,在安装效果 运行 时从初始渲染周期关闭并开始间隔。
  2. 您的点击更新程序应使用功能更新来更新先前状态的 clicks 计数。这涵盖了用户是否能够以某种方式更快地点击反应组件生命周期并在渲染周期中排队多个 clicks 更新。

T.J.的回答很好,但我认为 useState updater 函数是一个纯函数,中间的 alert(`user clicked ${currentClicks} times`); 是一个副作用和(恕我直言)应该避免。

解决方案

更新 handleOnAction 以使用功能状态更新。

const handleOnAction = () => {
  console.log("user did something", clicks);
  setClicks(clicks => clicks + 1);
};

我建议使用 React 引用和额外的 useEffect 来更新它,以保存 clicks 状态的缓存副本以供间隔回调引用。

const clicksRef = useRef();

useEffect(() => {
  clicksRef.current = clicks; // update clicks ref when clicks updates
}, [clicks]);

useEffect(() => {
  const timer = setInterval(
    () => alert(`user clicked ${clicksRef.current} times`), // use clicks ref instead
    1000 * 30
  );

  return () => {
    clearInterval(timer);
    // TJ already covered removing this extra state update
  };
}, []);