このバグはかなり紛らわしいです:
https://twitter.com/kentcdodds/status/1102659818660102145
setInterval(fn, 0)
によってスケジュールされたfn
が、 setRunning(false)
によって引き起こされる[running]
効果のクリーンアップの前にジャンプするために発生すると思います。 したがって、インターバルは引き続き発生し、イベント中に発生した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
キューに追加される順序がすべてです。
1 つのアプローチは、タイマーが実際に停止した場合、つまり実際に停止した場合にのみリセットをディスパッチすることです。 同じフレームで常にゼロを表示する場合は、実行が false のときに常にゼロを表示できます。
ただし、ほとんどの場合、これはリデューサーとしてより適切にモデル化されます。 レデューサーは、停止状態にあると判断した場合、更新の失効を簡単に拒否し、ロジックを実行して実際にリセットすることもできます。
最も参考になるコメント
1 つのアプローチは、タイマーが実際に停止した場合、つまり実際に停止した場合にのみリセットをディスパッチすることです。 同じフレームで常にゼロを表示する場合は、実行が false のときに常にゼロを表示できます。
ただし、ほとんどの場合、これはリデューサーとしてより適切にモデル化されます。 レデューサーは、停止状態にあると判断した場合、更新の失効を簡単に拒否し、ロジックを実行して実際にリセットすることもできます。