The upload-component module shipped. One big component, clear UI code, scattered state. An engineer reaches for extraction — “I'll pull the state into a custom hook.” The first shape that comes to mind is a chain of useState calls: files, progress, isUploading, error, isCancelled. Five lines, each a single-purpose boolean or number. Clean, local, looks right.

An hour later the walls arrive. Updating one file's progress re-renders the entire list of forty-seven files because every setProgress is a separate React round-trip. isUploading && isCancelled is a legal combination that nothing forbids — the hook invites callers to draw contradictions. Progress is a SINGLE number; with three concurrent uploads only the last writer wins. cancel() flips a boolean but the network keeps streaming bytes. No path runs on unmount; route changes leak everything.

Gauge · 5 statuses
  • idlepicked, waiting for start
  • uploadingbytes flowing
  • successsucceeded, cannot be un-done
  • errorfailed, awaiting retry or give-up
  • cancelleduser stopped it; network torn down
Events · 6 actions0/6 tried
Reducer log
  • tap any event to see the reducer fire

Tap each event on the console. Some drive the gauge forward; some are dropped because they make no sense in the current state (PROGRESS events have nowhere to land when the file hasn't started). Watch the log. Each event either advances the status or is recorded as an ignored no-op — the reducer's two canonical outcomes. Five statuses, six events. The entire hook's state machine in one panel.