React Hooks 多个带有单独倒计时的警报

React Hooks multiple alerts with individual countdowns

我一直在尝试构建一个具有多个警报的 React 应用程序,这些警报会在设定的时间后消失。示例:https://codesandbox.io/s/multiple-alert-countdown-294lc

import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";

import "./styles.css";

function TimeoutAlert({ id, message, deleteAlert }) {
  const onClick = () => deleteAlert(id);
  useEffect(() => {
    const timer = setTimeout(onClick, 2000);
    return () => clearTimeout(timer);
  });
  return (
    <p>
      <button onClick={onClick}>
        {message} {id}
      </button>
    </p>
  );
}
let _ID = 0;
function App() {
  const [alerts, setAlerts] = useState([]);
  const addAlert = message => setAlerts([...alerts, { id: _ID++, message }]);
  const deleteAlert = id => setAlerts(alerts.filter(m => m.id !== id));
  console.log({ alerts });
  return (
    <div className="App">
      <button onClick={() => addAlert("test ")}>Add Alertz</button>
      <br />
      {alerts.map(m => (
        <TimeoutAlert key={m.id} {...m} deleteAlert={deleteAlert} />
      ))}
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

问题是如果我创建多个警报,它会以错误的顺序消失。例如,测试 0、测试 1、测试 2 应该从测试 0、测试 1 等开始消失,但测试 1 首先消失,测试 0 最后消失。

我一直看到对 useRefs 的引用,但我的实现没有解决这个错误。


有了@ehab 的意见,我相信我能够朝着正确的方向前进。我在我的代码中收到了关于添加依赖项的进一步警告,但额外的依赖项会导致我的代码出现错误。最终我想出了如何使用 refs。我将其转换为自定义挂钩。

function useTimeout(callback, ms) {
  const savedCallBack = useRef();
  // Remember the latest callback
  useEffect(() => {
    savedCallBack.current = callback;
  }, [callback]);
  // Set up timeout
  useEffect(() => {
    if (ms !== 0) {
      const timer = setTimeout(savedCallBack.current, ms);
      return () => clearTimeout(timer);
    }
  }, [ms]);
}

好吧,你的问题是你在每个 re-render 上重新安装,所以基本上你在渲染时为所有组件重置计时器。

为了清楚起见,请尝试在警报组件中添加 {Date.now()}

      <button onClick={onClick}>
        {message} {id} {Date.now()}
      </button>

你每次都会注意到重置

因此,要在功能组件中实现这一点,您需要使用 React.memo

使您的代码正常工作的示例:

const TimeoutAlert = React.memo( ({ id, message, deleteAlert }) => {
  const onClick = () => deleteAlert(id);
  useEffect(() => {
    const timer = setTimeout(onClick, 2000);
    return () => clearTimeout(timer);
  });
  return (
    <p>
      <button onClick={onClick}>
        {message} {id}
      </button>
    </p>
  );
},(oldProps, newProps)=>oldProps.id === newProps.id) // memoization condition

第 2 次修复您的 useEffect,使其不在每个渲染器上 运行 清理功能

useEffect(() => {
  document.title = `You clicked ${count} times`;
}, [count]); // Only re-run the effect if count changes

最后是关于品味的东西,但你真的需要破坏 {...m} 对象吗?我会把它作为一个适当的道具传递,以避免每次都创建新对象!

你的代码有两处错误,

1) 你使用 effect 的方式意味着每次渲染组件时都会调用这个函数,但是显然根据你的用例,你希望这个函数被调用一次,所以把它改成

 useEffect(() => {
    const timer = setTimeout(onClick, 2000);
    return () => clearTimeout(timer);
  }, []);

将空数组添加为第二个参数,意味着您的效果不依赖于任何参数,因此只应调用一次。

您的删除警报取决于创建函数时捕获的值,这是有问题的,因为那时您没有数组中的所有警报,将其更改为

const deleteAlert =  id => setAlerts(alerts => alerts.filter(m => m.id !== id));

这是你的样本在我分叉后工作 https://codesandbox.io/s/multiple-alert-countdown-02c2h

这两个答案都遗漏了问题的几点,所以经过一段时间的挫折弄清楚这个问题后,这就是我得出的方法:

  • 有一个管理“警报”数组的挂钩
  • 每个“警报”组件管理其自身的销毁

但是,由于函数会随着每次渲染而变化,计时器会在每次道具更改时重置,这至少可以说是不可取的。

如果您试图遵守 eslint 详尽的 deps 规则,它还会增加另一层复杂性,您应该这样做,否则您将遇到状态响应问题。其他建议,如果您正在使用“useCallback”,那您找错地方了。

在我的例子中,我使用的是超时的“叠加层”,但您可以将它们想象成警报等。

打字稿:

// useOverlayManager.tsx
export default () => {
  const [overlays, setOverlays] = useState<IOverlay[]>([]);

  const addOverlay = (overlay: IOverlay) => setOverlays([...overlays, overlay]);
  const deleteOverlay = (id: number) =>
    setOverlays(overlays.filter((m) => m.id !== id));

  return { overlays, addOverlay, deleteOverlay };
};

// OverlayIItem.tsx
interface IOverlayItem {
  overlay: IOverlay;
  deleteOverlay(id: number): void;
}

export default (props: IOverlayItem) => {
  const { deleteOverlay, overlay } = props;
  const { id } = overlay;

  const [alive, setAlive] = useState(true);

  useEffect(() => {
    const timer = setTimeout(() => setAlive(false), 2000);
    return () => {
      clearTimeout(timer);
    };
  }, []);

  useEffect(() => {
    if (!alive) {
      deleteOverlay(id);
    }
  }, [alive, deleteOverlay, id]);

  return <Text>{id}</Text>;
};

然后渲染组件的位置:

  const { addOverlay, deleteOverlay, overlays } = useOverlayManger();
  const [overlayInd, setOverlayInd] = useState(0);

  const addOverlayTest = () => {
    addOverlay({ id: overlayInd});
    setOverlayInd(overlayInd + 1);
  };


  return {overlays.map((overlay) => (
            <OverlayItem
              deleteOverlay={deleteOverlay}
              overlay={overlay}
              key={overlay.id}
            />
          ))};

基本上:每个“叠加层”都有一个唯一的 ID。每个“overlay”组件管理自己的销毁,overlay 通过 prop 函数与 overlayManger 通信,然后 eslint exhaustive-deps 通过在 overlay 组件中设置一个“alive”状态 属性 来保持快乐,当更改为 false 时,将调用它自己的销毁。