当在点击处理程序中使用两个 useState 设置器时,只有一个有效
When two useState setters are being used in a click handler only one works
我在 React 中使用钩子制作了一个小秒表组件。这是演示问题的最少代码。
查看名为 resetTicks
的函数。它有两个设置器 setTicks
和 setTicking
,只有 setTicking
工作,即时钟暂停,有趣的是,如果我再次单击按钮,它只会重置时钟。我尝试重新排序对两个设置器的调用,但无济于事。
const StopWatch = () => {
const [ticks,setTicks] = useState(0);
const [ticking,setTicking] = useState(false);
useEffect(() => {
setTimeout(() => {
if (ticking) setTicks(ticks + 1);
},10);
},[ticks,ticking]);
const toggleTicking = e => {
setTicking(!ticking);
}
const resetTicks = e => {
// these two setters are causing the issue
// only the setTicking is actually showing effect. I have tried switching
// their order but nothing works.
setTicking(false);
setTicks(0);
}
const min = Math.floor(ticks / 6000);
const sec = Math.floor((ticks - (min * 6000)) / 100);
const centis = ticks % 100;
return (
<WatchWrapper>
<WatchDisplay>
<span>{min < 10 ? '0': ''}{min}</span>
<span>:</span>
<span>{sec < 10 ? '0': ''}{sec}</span>
<span>:</span>
<span>{centis < 10 ? '0' : ''}{centis}</span>
</WatchDisplay>
<WatchControls>
<WatchBtn onClick={toggleTicking}>
{ticking ? 'stop' : 'play_arrow'}
</WatchBtn>
<WatchBtn onClick={resetTicks}>refresh</WatchBtn>
</WatchControls>
</WatchWrapper>
)
}
这是一个棘手的问题,您应该从 console.log
:
开始了解发生了什么
true
56
true
57
true
58
true
59
false
0
false
60
它确实设置为 0,但显然在某些时候,旧的 setTimeout
是预定的火灾,它在 tick 的旧值上有一个 closure 60,因此它会将其重置为它。
增加超时说 3 秒做一个 console.log(ticking, ticks)
在渲染中,你应该更清楚问题是什么。
useEffect(() => {
const interval = setInterval(() => {
if (ticking) setTicks(prevState => prevState + 1);
}, 10);
return () => clearInterval(interval);
}, [ticking]);
这是因为 setTicks
setter 和 setTimeout
内部回调的异步调用之间的竞争条件。 setTicks
setter 更新了滴答计数,但旧的滴答计数已存储在 setTimeout
范围内。因此 setTimeout
引发回调,将 ticks
的旧值作为参数。您需要清理组件卸载时的 setTimeout
以防止:
useEffect(() => {
const timeout = setTimeout(() => {
if (ticking) setTicks(ticks + 1);
}, 10);
return () => clearTimeout(timeout);
}, [ticking, ticks]);
为确保不存在竞争条件,您可以尝试通过为计时器创建一个 React 引用来在 resetTicks 之前重置 setTimeout。
const [ticks,setTicks] = React.useState(0);
const [ticking,setTicking] = React.useState(false);
const timer = React.createRef();
React.useEffect(() => {
timer.current = setTimeout(() => {
if (ticking) setTicks(ticks + 1);
},10);
},[ticks,ticking, timer]);
const toggleTicking = e => {
setTicking(!ticking);
}
const resetTicks = e => {
clearTimeout(timer.current);
setTicking(false);
setTicks(0);
}
在此处使用 Codesandbox 进行测试:
我在 React 中使用钩子制作了一个小秒表组件。这是演示问题的最少代码。
查看名为 resetTicks
的函数。它有两个设置器 setTicks
和 setTicking
,只有 setTicking
工作,即时钟暂停,有趣的是,如果我再次单击按钮,它只会重置时钟。我尝试重新排序对两个设置器的调用,但无济于事。
const StopWatch = () => {
const [ticks,setTicks] = useState(0);
const [ticking,setTicking] = useState(false);
useEffect(() => {
setTimeout(() => {
if (ticking) setTicks(ticks + 1);
},10);
},[ticks,ticking]);
const toggleTicking = e => {
setTicking(!ticking);
}
const resetTicks = e => {
// these two setters are causing the issue
// only the setTicking is actually showing effect. I have tried switching
// their order but nothing works.
setTicking(false);
setTicks(0);
}
const min = Math.floor(ticks / 6000);
const sec = Math.floor((ticks - (min * 6000)) / 100);
const centis = ticks % 100;
return (
<WatchWrapper>
<WatchDisplay>
<span>{min < 10 ? '0': ''}{min}</span>
<span>:</span>
<span>{sec < 10 ? '0': ''}{sec}</span>
<span>:</span>
<span>{centis < 10 ? '0' : ''}{centis}</span>
</WatchDisplay>
<WatchControls>
<WatchBtn onClick={toggleTicking}>
{ticking ? 'stop' : 'play_arrow'}
</WatchBtn>
<WatchBtn onClick={resetTicks}>refresh</WatchBtn>
</WatchControls>
</WatchWrapper>
)
}
这是一个棘手的问题,您应该从 console.log
:
true
56
true
57
true
58
true
59
false
0
false
60
它确实设置为 0,但显然在某些时候,旧的 setTimeout
是预定的火灾,它在 tick 的旧值上有一个 closure 60,因此它会将其重置为它。
增加超时说 3 秒做一个 console.log(ticking, ticks)
在渲染中,你应该更清楚问题是什么。
useEffect(() => {
const interval = setInterval(() => {
if (ticking) setTicks(prevState => prevState + 1);
}, 10);
return () => clearInterval(interval);
}, [ticking]);
这是因为 setTicks
setter 和 setTimeout
内部回调的异步调用之间的竞争条件。 setTicks
setter 更新了滴答计数,但旧的滴答计数已存储在 setTimeout
范围内。因此 setTimeout
引发回调,将 ticks
的旧值作为参数。您需要清理组件卸载时的 setTimeout
以防止:
useEffect(() => {
const timeout = setTimeout(() => {
if (ticking) setTicks(ticks + 1);
}, 10);
return () => clearTimeout(timeout);
}, [ticking, ticks]);
为确保不存在竞争条件,您可以尝试通过为计时器创建一个 React 引用来在 resetTicks 之前重置 setTimeout。
const [ticks,setTicks] = React.useState(0);
const [ticking,setTicking] = React.useState(false);
const timer = React.createRef();
React.useEffect(() => {
timer.current = setTimeout(() => {
if (ticking) setTicks(ticks + 1);
},10);
},[ticks,ticking, timer]);
const toggleTicking = e => {
setTicking(!ticking);
}
const resetTicks = e => {
clearTimeout(timer.current);
setTicking(false);
setTicks(0);
}
在此处使用 Codesandbox 进行测试: