使用 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];
};
非常努力地为此代码插入 运行 代码片段,但没有成功,我对此感到抱歉。
我创建了这个自定义挂钩以了解什么是挂钩。
有效,但存在逻辑问题。
- 不能 'clearInterval()' 在第二次渲染之前 (stopCounting())
- 逻辑上,可以通过代码设置多个计数时间表(scheduleCounting())
- 也许钩子设计一开始就错了?
对于第一个问题,我无法清除在 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
)。
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];
};
非常努力地为此代码插入 运行 代码片段,但没有成功,我对此感到抱歉。
我创建了这个自定义挂钩以了解什么是挂钩。
有效,但存在逻辑问题。
- 不能 'clearInterval()' 在第二次渲染之前 (stopCounting())
- 逻辑上,可以通过代码设置多个计数时间表(scheduleCounting())
- 也许钩子设计一开始就错了?
对于第一个问题,我无法清除在 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
)。