skip to content

Scheduling, Batching & Purity

What happens between a state write and the work it causes: queued or immediate writes, writes coalescing into one pass, the flush, and why output code must be pure. Stale-read bugs come from here.

on this pageshow

questions

5

Why do component frameworks coalesce several state writes made during one event into a single update pass?

level: juniorimportance: must knowfreq 70%

answer

  1. one event, one update
  2. queue first, flush later
  3. half-updated screens are the real risk
  4. the flush point differs by runtime
  5. a synchronous escape hatch exists

basics

~10 s

So the user never sees a half-updated screen and the work is paid for once. The runtime marks what changed, lets the event's code finish, then runs one pass that covers every write.

solid answer

~40 s

A write records a new value and asks for output work. If that work ran on every write, three writes in one handler would produce three passes, each describing a screen that is only partly correct, and the user could see the intermediate ones. So the runtime queues the requests instead: it marks the affected instances or computations as dirty, lets the handler run to the end, and flushes one pass afterwards - at the end of the event's synchronous work, on a microtask, on the next tick, or before the next frame, depending on the runtime. The result is fewer passes and one consistent screen. The price is that output is not updated on the line after the write, which is why most runtimes also provide a synchronous-flush escape hatch.

go deeper

for a junior

Recall the rule: many writes in one event, one update. Make the writes you need and let the runtime decide when to render, rather than trying to force each one through.

for a middle

Explain the queue: dirty marking, the flush point your runtime uses, and the ordering inside a pass. Be precise that coalescing never drops a write or merges values.

for a senior

Show the consequences you police: code assuming output is current on the line after a write, and handlers that force synchronous flushes and turn one pass per event into many.

for a principal

Treat the flush point as a contract the codebase leans on. When a team routinely defeats it with synchronous flushes, the real problem is usually where the work lives, not the batching.

Coalescing - often called batching - is the rule that the writes made during one event produce **one** update, not one update per write. It is the reason a handler that changes three pieces of state does not re-render three times. ## What a write actually does A write is a request, not the work: - it records the new value (immediately into a cell, or as a queued change against the next snapshot, depending on the reactivity model); - it marks something as dirty - the owning instance in a runtime that re-runs component functions, or the specific dependent computations in a fine-grained one; - it makes sure a flush is scheduled, if one is not scheduled already; - it returns, so the rest of the handler runs with no rendering in between. That last point is the whole mechanism. Because the write returns instead of rendering, the handler can make five more writes and the runtime still has only one flush pending. ## Why coalescing is a correctness rule, not only an optimisation The performance story is obvious: one pass instead of many, and one set of host mutations instead of several. The correctness story matters more. Consider a handler that clears an error, sets a new list and moves the selected index. Rendered after the first write alone, the screen shows a cleared error beside the old list; after the second, a new list with a selection pointing at the wrong row. Those states are not merely wasteful, they are **invalid** - combinations your output code was never written to describe. Coalescing means no pass ever observes one. ## Where the flush lands Runtimes differ, and the differences are observable: | flush point | what schedules it | what you may assume afterwards | |---|---|---| | end of the event's synchronous work | the runtime's own handling of the event | output applied before control returns to the platform | | a microtask after the current work | queued the first time something is marked dirty | output applied before the next event is dispatched | | the next tick of the runtime's own queue | the runtime's scheduler | output applied, with an explicit signal you can await | | before the next frame is painted | a frame callback | output applied in time to be painted, but later than a microtask | In every case the ordering inside the pass is what you rely on: an instance is processed once, parents before children, derived values before the effects that read them. That ordering is why a child never renders from a parent value that the same pass is about to change. ## What coalescing does not do - It does not drop writes. Every write applies; for one cell, the last plain write wins because it was written last, not because the runtime discarded the earlier ones. - It does not merge values. A write expressed as a function of the previous value still composes with the queued writes before it. - It does not debounce the user. Coalescing collapses the writes of *one* event; two separate clicks are two events and normally two passes. - It does not make output readable early. Nothing is applied until the flush, whatever the write model. ## Writes outside an event handler Writes made in a promise continuation, a timer callback or a stream subscription are coalesced by most current runtimes as well: the queue is drained at the end of that turn, so several writes there still produce one pass. This was not always true - some runtimes historically coalesced only inside their own event handling and flushed each write elsewhere, which is why older codebases sometimes wrap such writes in an explicit batch helper. If you are unsure which behaviour you have, count the passes rather than assume. ## The escape hatch, and when to reach for it Because coalescing defers the output, runtimes offer a way to flush synchronously: apply the pending updates now, before the next line runs. It is the right tool when a value must exist before the user could see an inconsistent frame - measuring freshly revealed output to position something, or restoring a scroll offset inside the same interaction. It is the wrong default. Each synchronous flush gives up the coalescing for that event, and one inside a handler that fires on every pointer move converts one pass per event into many, which is a classic cause of stutter. ## How to see it for yourself 1. Count executions of the output function, or of the runtime's after-update callback, while a handler makes several writes. 2. Move the same writes into a timer callback and count again; that tells you whether your runtime coalesces outside its own event handling. 3. Wrap one write in the synchronous-flush escape hatch and watch the count rise - the clearest demonstration of what coalescing was buying.

  • Does coalescing merge the writes themselves, or only the work they cause?
    Only the work. Every write still applies in order: for a single cell the last plain write wins because it came last, and a write expressed as a function of the previous value composes with those queued before it. Coalescing decides how many passes run, never which value ends up stored.
  • What happens to writes made in an asynchronous callback rather than an event handler?
    Most current runtimes coalesce those too, draining the queue at the end of that turn, so several writes in one callback still produce one pass. Some runtimes historically flushed each write outside their own event handling, which is why older code sometimes batches manually. Count the passes if you need to know.
  • How would you check whether two writes produced one pass or two?
    Count rather than guess: increment a counter in the output function or in the runtime's after-update callback, or watch which host nodes get touched. One pass shows a single execution per affected instance no matter how many writes fed it.

saying these in an interview costs you the question

  • Says the framework discards some writes to save work
  • Believes every individual write paints a frame the user sees
  • Thinks coalescing merges values, so earlier writes are lost
  • Calls a synchronous flush the normal way to apply an update
  • Assumes the flush point is the same in every runtime
  • Confuses coalescing one event with debouncing the user's input
open as a page

Why does reading state back right after writing it give the old value in some component frameworks and the new one in others?

level: middleimportance: must knowfreq 74%

basics

~20 s

Two write models. A runtime that re-runs components treats each pass's state as a fixed snapshot, so a queued write cannot change the value that pass already read. A fine-grained runtime updates the cell immediately and merely schedules dependents.

open as a page

Why must the function that produces a component's output be pure, and what breaks when it is not?

level: middleimportance: should knowfreq 64%

basics

~20 s

Because the runtime decides how often it runs: it may run it again for the same state, throw the result away, or skip it. Side effects inside it therefore happen an unpredictable number of times.

open as a page

A state write reveals a panel and the next line measures it and finds nothing - why, and what is the correct fix?

level: seniorimportance: should knowfreq 58%

basics

~20 s

The write only queued an update, so the host tree still holds the previous output when the next line runs. Move the read into the runtime's after-update signal, or force a synchronous flush for that one read.

open as a page

When is a runtime that ranks urgent updates above deferred work, and can abandon a half-done pass, worth adopting?

level: principalimportance: should knowfreq 44%

basics

~20 s

When measurement shows an interaction losing the main thread to a heavy update. Ranking finishes the urgent pass first and abandons stale deferred work, at the price of repeated passes, strict purity, and harder reasoning about what ran.

open as a page