skip to content

Writes & Optimistic Updates

Writing a change back and keeping the tree consistent after: a pending or failed write, an optimistic patch that rolls back, invalidation versus patching the cache. The rollback path gets probed.

on this pageshow

questions

5

When a component triggers a write to the server, which states does that write pass through, and why disable the trigger while it is pending?

level: juniorimportance: must knowfreq 72%

answer

  1. a write is a state machine
  2. idle, pending, settled
  3. the trigger is part of the state
  4. duplicate write, not duplicate render
  5. clear pending on the failure path too

basics

~20 s

A write passes through idle, pending, then success or failure, and the component renders each. Disabling the trigger while pending stops a duplicate write the server may apply twice, whose two responses can also land in either order.

solid answer

~50 s

A write is a small state machine, not a fire-and-forget call. Before it starts the component is idle; once triggered it is pending and should show that on the control that started it; then it settles into success or failure, and both need a rendered outcome — a rejected write that leaves the screen looking unchanged is the worst of the four, because the user believes it saved. I keep that state keyed by the thing the write targets rather than in one screen-wide `loading` flag, so a saving row disables only its own control. Disabling while pending is not cosmetic: a second activation sends a second write the API may apply twice, and the two responses then race. Clear the pending state on the failure path too, and re-enable so the user can retry.

go deeper

for a junior

Name the states out loud — idle, pending, success, failure — and remember that the control which started the write is part of that state. A write with no pending feedback and no visible failure reads as a broken screen.

for a middle

Explain where the pending state lives, why it is keyed per write rather than per screen, and show that you clear it on the failure path as well as the success path.

for a senior

Show the operational instincts: a disabled trigger closes only the local duplicate path, failures must be rendered and retryable, and several writes can be pending at once on one list.

for a principal

Frame it as a contract — the client guards usability, the API guarantees a change is applied once. Settle that line across teams before every screen invents its own defence.

## A write is a state machine, not a function call Reading data and writing it back are both requests, but they differ in one decisive way: a write **changes something**. A read that runs twice wastes bandwidth; a write that runs twice can create two records, send two invitations, or apply a charge again. That asymmetry is why every component framework's answer to "how do I send a change?" converges on the same shape: the request's progress becomes **state the view renders**, and the control that started it becomes part of that state. Frameworks give you very little here for free. A runtime re-renders when the state it tracks changes; it has no idea a request is outstanding. Whether the progress lives in local component state, in a shared store, or in a server-state cache that tracks writes for you, the same states exist and the same mistakes are available. ## The states and what each one owes the user | state | what the view shows | what the trigger does | | --- | --- | --- | | idle | the current value, nothing extra | enabled | | pending | the current value plus a busy marker on the control | disabled, or ignores further activation | | success | the new value, confirmed if the change is not self-evident | enabled again | | failure | the old value, a readable reason, and a way to try again | enabled again | Two rows carry most of the bugs: - **pending** is the state people skip. Without it the screen is inert between the activation and the response, and the user's only recourse is to press again. - **failure** is the state people swallow. A write that was rejected but looks like nothing happened teaches the user that the change was saved. ## Why the trigger is disabled while the write is pending Disabling is not decoration. Three things follow from a second activation: 1. **A second request goes out.** The API may apply both, so the user gets two of whatever the write produces. 2. **Two responses race.** If the component writes each response into state as it lands, the value that survives depends on network timing rather than on intent. 3. **Your bookkeeping doubles.** If the write took an optimistic patch or saved a previous value, there are now two of them over one entity — a harder problem than the duplicate request itself. Disabling closes the fastest of those paths, and only locally. The user can still act in a second tab, a transport-level retry can resend the request, and a fast activation can land before the pending state has committed to the view. So state the boundary plainly: **the client's guard is a usability measure; the guarantee that a change is applied only once belongs to the API's contract.** Claiming that a disabled control makes a write safe is the answer that gets marked down. ## Where the pending state should live One screen-wide flag is the common shortcut and a poor one: - it disables unrelated controls while a single row is saving; - it cannot represent two writes pending at once, which is ordinary on a list; - it only lets the view ask "is anything happening?" when the view needs "is *this* row saving?". Keep the state keyed by the entity the write targets — the row's identifier — so the view disables exactly the control that started it and shows progress in place. Frameworks differ in how such state is held and observed: one that re-runs the component function reads a fresh value on each render, one with fine-grained tracking notifies only the control bound to that key, and a compile-time reactive model rewrites the read into a subscription. None of that changes what has to be stored. ## Clearing the state on every path The detail that fails in review is the last one: pending must be cleared on the failure path as well, including when the request rejects instead of returning a failure payload. A write left permanently pending disables its own trigger forever and the user's only exit is a reload. Put the clear in the branch that always runs, and treat "the button is still disabled" as the symptom of a missing failure branch. One more habit worth naming: keep the value on screen while the write is pending instead of replacing the content with a placeholder. The user is looking at the thing they just changed; blanking it to prove work is happening costs them their place for no information gain.

  • Why is one screen-wide loading flag a poor home for a write's pending state?
    Because pending is per-write, not per-screen. A single flag disables unrelated controls while one row saves, cannot represent two writes pending at once, and leaks between surfaces that share it. Key the state by the entity the write targets so the view can disable exactly the control that started it and show progress in place.
  • Does disabling the trigger while pending make the write safe to apply only once?
    No. It closes the fastest duplicate path in one view, but the user can act in a second tab, a transport-level retry can resend the request, and an activation can land before the pending state commits. Client-side disabling is a usability guard; applying a change exactly once is a property the API has to guarantee on its side.

saying these in an interview costs you the question

  • Send the request and move on; a write needs no pending state.
  • A disabled control is cosmetic, so skipping it is harmless.
  • Two identical activations are safe because the request is the same.
  • The framework will surface a failed write on its own.
  • One screen-wide loading flag can drive every write on the page.
open as a page

What does an optimistic update write into the cache before the server answers, and what does it need to roll back?

level: middleimportance: must knowfreq 64%

basics

~20 s

An optimistic update patches the cached copy with the client's predicted result before the request settles. Rollback needs the previous value of exactly the entries it touched plus the patch's own identity; success needs reconciliation with the server's representation.

open as a page

After a successful write, when would you patch the cached entry from the response instead of invalidating it and refetching?

level: middleimportance: should knowfreq 56%

basics

~20 s

Patch when the response carries the complete updated form of exactly what changed and nothing derived elsewhere depends on it. Invalidate and refetch when the write moves counts, ordering, pages or other entities, or when its response says nothing useful.

open as a page

Two writes to the same item are in flight and the row flickers back to an old value; how do you make the settled state deterministic?

level: seniorimportance: should knowfreq 48%

basics

~20 s

Stop treating each response as the final word. Track how many writes to that entity are in flight, derive the view from a base value plus ordered pending patches, ignore or queue superseded writes, and revalidate once nothing is pending.

open as a page

How do you decide which writes in a product get an optimistic update and which wait for the server?

level: principalimportance: should knowfreq 40%

basics

~20 s

Weigh how predictably the client can compute the result against the cost of being wrong. Optimism fits reversible, self-contained, frequent changes; a write the server decides, or whose reversal would confuse or cost, should wait.

open as a page