React 17 batched state updates inside event handlers and not inside promises, timeouts or native handlers; 18 batches everywhere.
async function save() {
setSaving(true)
await api.save(draft)
setSaving(false)
setSaved(true)
// 17: two renders. 18: one.
}
// the code that breaks: anything reading the DOM between
// two setState calls and expecting the first to have
// been applied.
// the escape hatch, which should be rare
import { flushSync } from 'react-dom'
flushSync(() => setSaving(false))
Fewer renders is the intended benefit and the breakage is in code that had come to depend on the inconsistency — usually a measurement taken between two updates, or a third-party widget synchronised by hand. flushSync exists for those and using it more than once or twice in an application is a sign that something else is wrong.