首页 > 解决方案 > 在 setInterval 中反应 setState

问题描述

我有一个组件,其中存储了一个对象数组,例如:

const [items, setItems] = React.useState([]);

我可以通过单击一个按钮向该数组添加一些元素:

const addItem = () => {
  setItems(items => [ ...items, { value: 'foo', color: 'yellow' }]);
};

现在,我想让列表中的每个项目每 500 毫秒从黄色闪烁到红色,持续 5 秒。所以我创建了一个通用计时器函数来帮助我,例如:

const timer = (timeout: number, interval: number, callback: (n: number) => void) => {
  const start = new Date().getTime();
  let n = 0;
  const _timer = setInterval(() => {
    if ((new Date().getTime() - start) >= timeout) {
      clearInterval(_timer);
    } else {
      callback(++n);
    }
  }, interval);
};

我通过在我的组件中执行以下操作来触发该计时器:

React.useEffect(() => {
  // I also store the previous state, so that I trigger a timer only when new items
  // are added to my array. Yeah not ideal...
  if (items.length > previousItems.length) {
     const lastItem = items.length - 1;
     timer(5000, 500, (n: number) => {
       const updatedItems = [ ...items ];
       updatedItems[index].color = n % 2 == 0 ? 'yellow' : 'red';
       setItems(items => updatedItems);
     });
  }
});

如果我只将项目一个一个地添加到列表中(例如,计时器在添加新项目之前完成),这种有缺陷的方法可以正常工作。但是同时添加多个item的时候就彻底坏了:第一个定时器已经创建并没有考虑到后面添加的所有item。我知道这种方法根本不理想,但是如果您有更好的方法来实现我想要的,我将不胜感激。谢谢。

标签: reactjstypescriptsetintervaluse-state

解决方案


不要使用 old items,而是使用你的 setter 回调传递的那个,请参阅***评论:

React.useEffect(() => {
    if (items.length > previousItems.length) {
        const lastItem = items.length - 1;
        timer(5000, 500, (n: number) => {
            // *** Move the entire update into the callback
            setItems(items => {
                const updatedItems = [ ...items ]; // <=== Now this is using the up-to-date `items`
                updatedItems[index].color = n % 2 == 0 ? 'yellow' : 'red';
                return [updatedItems];
            });
        });
    }
});

您还需要确保在组件卸载或重新渲染时取消间隔计时器,因为您的代码在重新渲染时启动一个计时器(如果长度检查通过)。这些将很快叠加起来进行多次重叠更新。

你可以timer返回一个cancel函数:

const timer = (timeout: number, interval: number, callback: (n: number) => void) => {
    const start = new Date().getTime();
    let n = 0;
    const _timer = setInterval(() => {
        if ((new Date().getTime() - start) >= timeout) {
            clearInterval(_timer);
        } else {
            callback(++n);
        }
    }, interval);
    return () => clearInterval(_timer); // ***
};

然后将其用作useEffect清理回调:

React.useEffect(() => {
    if (items.length <= previousItems.length) {
        return;
    }
    const lastItem = items.length - 1;
    const cancel = timer(5000, 500, (n: number) => {
//  ^^^^^^^^^^^^
        // *** Move the entire update into the callback
        setItems(items => {
            const updatedItems = [ ...items ]; // <=== Now this is using the up-to-date `items`
            updatedItems[index].color = n % 2 == 0 ? 'yellow' : 'red';
            return [updatedItems];
        });
    });
    return cancel; // ***
});

推荐阅读