Эта ошибка довольно сбивает с толку:
https://twitter.com/kentcdodds/status/1102659818660102145
Я думаю, это происходит потому, что fn
запланированный setInterval(fn, 0)
перескакивает перед очисткой эффекта [running]
вызванного setRunning(false)
. Таким образом, интервал все еще срабатывает, перезаписывая setLapse(0)
, произошедшее во время события, его setLapse(someValue)
.
Это напоминает мне проблему, описанную в https://github.com/facebook/react/issues/14750#issuecomment -460409609, или, по крайней мере, ее часть:
Фактически, эта проблема существует даже для обычных нажатий клавиш React (и других «дискретных» событий). Решением этого будет сброс пассивных эффектов до того, как мы получим дискретное событие.
Но здесь кажется, что этого недостаточно, потому что эффект переворачивается в результате щелчка, а не до него. Так должен ли setState
внутри дискретного события также сбрасывать пассивный эффект? Похоже, что нет. (Это лишило бы смысла их задерживать.)
Итак, это работает, как задумано, и исправление составляет всего useLayoutEffect
когда время имеет значение? Или решение rAF?
Проблема в том, что сам таймер не является дискретным событием. Дискретные события гарантируются только в отношении других дискретных событий. Я не думаю, что это то, что вам здесь нужно.
Остановка таймера является асинхронной операцией, поскольку все установленные состояния являются асинхронными. Так что что-то еще может войти, прежде чем оно будет смыто.
Это тот случай, когда useLayoutEffect на самом деле не решает проблему полностью. Конечно, не в параллельном режиме, но в режиме синхронизации это тоже отрывочно. Если бы это был не таймер, а, скажем, событие фокуса, оно могло бы срабатывать в пакете до того, как оно будет сброшено, что будет иметь ту же проблему.
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
Все дело в том, в каком порядке вещи добавляются в очередь.
Один из подходов состоит в том, чтобы отправить сброс только после того, как таймер действительно остановится, то есть в результате. Если вы хотите, чтобы он всегда отображал ноль в одном и том же кадре, вы всегда можете отображать ноль, когда выполнение ложно.
Однако, как почти всегда, это лучше смоделировать как редуктор. Редуктор может легко отказаться от обновления обновлений, если он думает, что он остановлен, а также выполнить логику для его фактического сброса.
Я понимаю теперь. Спасибо.
https://mobile.twitter.com/dan_abramov/status/1104069236132057089
https://codesandbox.io/s/23m1659rlp
Самый полезный комментарий
Один из подходов состоит в том, чтобы отправить сброс только после того, как таймер действительно остановится, то есть в результате. Если вы хотите, чтобы он всегда отображал ноль в одном и том же кадре, вы всегда можете отображать ноль, когда выполнение ложно.
Однако, как почти всегда, это лучше смоделировать как редуктор. Редуктор может легко отказаться от обновления обновлений, если он думает, что он остановлен, а также выполнить логику для его фактического сброса.