Este bug é muito confuso:
https://twitter.com/kentcdodds/status/1102659818660102145
Eu acho que isso acontece porque fn
programado por setInterval(fn, 0)
pula na frente da limpeza de efeito [running]
causada por setRunning(false)
. Portanto, o intervalo ainda dispara, sobrescrevendo setLapse(0)
que aconteceu durante o evento com seus setLapse(someValue)
.
Isso me lembra do problema descrito em https://github.com/facebook/react/issues/14750#issuecomment -460409609, ou pelo menos parte dele:
Na verdade, esse problema existe mesmo para pressionamentos de tecla React regulares (e outros eventos “discretos”). A solução para isso seria eliminar os efeitos passivos antes de obter um evento discreto.
Mas aqui, parece que isso não seria suficiente porque o efeito vira como resultado do clique, não antes dele. Portanto, setState
dentro de um evento discreto também deve liberar o efeito passivo? Parece que não. (Isso anularia o propósito de atrasá-los.)
Então, isso está funcionando conforme planejado, e a correção é apenas useLayoutEffect
quando o tempo é importante? Ou a solução rAF?
O problema é que o próprio cronômetro não é um evento discreto. Eventos discretos são garantidos apenas em relação a outros eventos discretos. Não acho que é isso que você quer aqui.
Parar o cronômetro é uma operação assíncrona, pois todos os estados definidos são assíncronos. Portanto, algo mais pode entrar antes de ser liberado.
Este é um caso em que useLayoutEffect não corrige totalmente o problema. Certamente não no modo simultâneo, mas também é incompleto no modo de sincronização. Se não fosse um cronômetro, mas digamos um evento de foco, ele pode disparar em um lote antes de ser liberado, o que teria o mesmo problema.
setRunning(false); // I would like to add a stop of this timer to the queue to be performed later
setLapse(0); // I would like to add an operation to set lapse to zero later
// lots of random stuff that can happen before the batch flushes
// This might also queue an operation to set lapse to something else
// actual rendering
// If concurrent mode, lots of other random stuff that can happen while rendering
// This might also queue an operation to set lapse to something else
// Actually do all that work in order
É tudo sobre a ordem em que as coisas são adicionadas à fila.
Uma abordagem é despachar o reset apenas quando o cronômetro realmente parar, ou seja, no efeito. Se você quiser que ele sempre exiba zero no mesmo quadro, poderá sempre exibir zero quando a execução for falsa.
No entanto, como quase sempre, é melhor modelado como um redutor. O redutor pode facilmente recusar as atualizações quando ele pensa que está em um estado parado e também executar a lógica para realmente redefini-lo.
Agora eu entendi. Obrigado.
https://mobile.twitter.com/dan_abramov/status/1104069236132057089
https://codesandbox.io/s/23m1659rlp
Comentários muito úteis
Uma abordagem é despachar o reset apenas quando o cronômetro realmente parar, ou seja, no efeito. Se você quiser que ele sempre exiba zero no mesmo quadro, poderá sempre exibir zero quando a execução for falsa.
No entanto, como quase sempre, é melhor modelado como um redutor. O redutor pode facilmente recusar as atualizações quando ele pensa que está em um estado parado e também executar a lógica para realmente redefini-lo.