使用 setInterval 和陈旧的闭包时,对自定义挂钩做出更好的解决方案

react better solution for custom hooks when using setInterval and stale closures

const useCounterInterval = (
  startAt = 0,
  countingStep = 1,
  countingTime = 1000
) => {
  const [counter, setCounter] = useState(startAt);
  const [timerId, setTimerId] = useState(null);

  const scheduleCounting = useCallback(() => {
    if (timerId !== null) return;
    setTimerId(
      setInterval(() => {
        setCounter(counter => counter + countingStep);
      }, countingTime)
    );
  }, [timerId, countingStep, countingTime]);

  const clearSchedule = useCallback(() => {
    clearInterval(timerId);
    setTimerId(null);
  }, [timerId]);

  const stopCounting = useCallback(() => {
    clearSchedule();
  }, [clearSchedule]);

  const resetCounting = useCallback(() => {
    clearSchedule();
    setCounter(0);
  }, [clearSchedule]);

  useEffect(() => {
    scheduleCounting();
    // scheduleCounting();
    // clearSchedule();
  }, []);

  return [counter, scheduleCounting, stopCounting, resetCounting];
};

非常努力地为此代码插入 运行 代码片段,但没有成功,我对此感到抱歉。

我创建了这个自定义挂钩以了解什么是挂钩。

有效,但存在逻辑问题。

  1. 不能 'clearInterval()' 在第二次渲染之前 (stopCounting())
  2. 逻辑上,可以通过代码设置多个计数时间表(scheduleCounting())
  3. 也许钩子设计一开始就错了?

对于第一个问题,我无法清除在 timerId[=58= 的第二次渲染之前由 useEffect() 启动的第一个预定计时器] 设置了新值。因为很明显,stopCounting() 关闭了值为 null 的 timerId。 当时机成熟时,我可以通过点击一个按钮来通知(取决于哪个事件 stopCounter() 是有界的)。我现在不知道如何解决这个问题。我可以从 useEffect() 中删除 scheduleCounting() 但这不是一个选项我想知道是否有办法解决这个问题。

其次,我可以通过在 useEffect() 中调用 scheduleCounting() 来设置多个计数计划。想一想,我可能会通过非常快速地单击按钮来多次调用 scheduleCounting(),但似乎我不能,这与调用 stopCounting()[ 不同=58=] 在第二次渲染之前。但逻辑是我可以做到吗?

所以,这里的问题是 scheduleCounting() 关闭了相同的 timerId 值为 null

const scheduleCounting = useCallback(() => {
if (timerId !== null) return; // bypassing this line of code

这里的解决方案是利用 useRef()。而不是检查 timerId 是否为 null,其值可能会根据引用它的函数的创建时间而有所不同。我可以简单地

const schedulerState = useRef(false)
if (schedulerState.current === true) return;

它确保每个 scheduleCounting() 在任何时候都引用相同的最新值。但是使用 flag(ref, state) 会使代码的可读性降低。我想让自己远离使用旗帜。我想知道是否还有另一种 反应式 方法来处理这个问题?

我不知道我哪里错了。不能停止思考,如果我做错了,我可能会走错路。需要一些指导。

从设计的角度来看,我不会保持 timerId 状态。相反,我有状态 isCounting:

const useCounterInterval = (
  startAt = 0,
  countingStep = 1,
  countingTime = 1000
) => {
  const [isCounting, setIsCounting] = useState(false);
  const [currentTick, setTick] = useState(0);
  
  const startCounting = useCallback(() => { setIsCounting(true); }, []);
  const stopCounting = useCallback(() => { setIsCounting(false); }, []);
  const resetCounting = useCallback(() => { setTick(0); }, []);
  
  useEffect(() => {
    resetCounting();
    if (isCounting) {
      const timerId = setInterval(() => {
        setTick(i => i + 1);
      }, countingTime);

      return () => { clearInterval(timerId); };
    }
  }, [isCounting, resetCounting, countingTime, startAt, countingStep]);

  return [
    currentTick * countingStep + startAt, 
    startCounting, 
    stopCounting, 
    resetCounting,
    isCounting
  ];
};

不存储 timerId 而是将其保存在 useEffect 的闭包中更容易。最好 return 是否正在计数(通过 returning isCounting)。