Skip to content
Back to Blog

Pause the watchers, keep the state ​

Pause the watchers, keep the state

Disposal is a reset told the story of $stopEffects(): one verb that stops an instance's watchers, frees its cells, and leaves the object in its pre-touch state, ready to live again. Full death, cleanly conjugated.

But writing it down exposed a bundling. The verb performs two acts — silencing (stop the effects) and forgetting (clear the state) — and nothing says they must travel together. Sometimes the right death is smaller: an entity leaves the visible world but keeps its data; a feed pauses its rendering while its buffer stays warm; a model is archived mid-thought and must wake up exactly where it stopped. Silence, without forgetting.

ivue 2.4.0 unbundles them, as one option on the verb that already existed:

ts
session.$stopEffects();                 // stop + forget (unchanged)
session.$stopEffects({ reset: false }); // stop the watchers ONLY

With { reset: false } every watcher dies at once — same scope, same precision — but no cache is touched. Cell identity persists, current values persist, and surviving computeds keep evaluating.

The part that had to be checked ​

That last claim was the design's one real hazard. Cells materialize in whatever effect scope is active at first touch — so a computed() first read inside the instance's own $watchEffect is born inside the scope being stopped. If stopped scopes dragged their computeds into permanent staleness, keep-state mode would be a silent-corruption factory.

Measured instead of assumed: on Vue 3.5.41 and on 3.6.0-rc.5 — the alien-signals rewrite, where exactly this machinery changed — a computed born in a stopped scope keeps evaluating correctly on read, and a new watcher in a fresh scope tracks it and fires. Pull-based derivations outlive their birth scope. The option is safe on both engines, verified before it shipped.

Resume costs no API ​

The resume half of the lifecycle needed no engine surface at all — only a convention the standard now teaches: watchers live behind a startWatchers() method, and the constructor calls it once.

ts
class $Feed {
  constructor() {
    this.startWatchers();
  }

  startWatchers() {
    this.$watchEffect(() => this.render());
  }

  suspend() {
    this.$stopEffects({ reset: false }); // watchers die, state stays
  }

  resume() {
    this.startWatchers(); // fresh scope, same cells
  }

  render() {
    /* ... */
  }
}

resume() is just startWatchers() again — the same method the constructor already used, now running in a fresh lazy scope over the same cells. One option flag plus one convention, and the whole suspend/resume lifecycle exists. Nothing was designed for it; it fell out of pieces that already had one job each.

Three states, one verb ​

The lifecycle now reads as three states with honest names:

statewatcherscellshow you get there
alivelivelivenew, or any touch
suspendeddeadintact, current values$stopEffects({ reset: false })
resetdeadcleared — next touch re-initializes$stopEffects()

Suspended is the state for windowing side effects over retained data — the complement of what the flyweight grid does with memory. There, reactive cells exist only for observed rows; here, watchers exist only for active entities, while their state stays resident. Both are the same invariant pointed at different resources: everything costs proportional to what's engaged, nothing costs proportional to what exists.

The price of the whole feature: one options parameter, two new specs (198 total, coverage still 100%), and 13 bytes of gzipped engine. The core stands at 1,125 bytes. Silence, forgetting, and the space between them — still one kilobyte.

Last updated:

Evgeny Kalashnikov
AuthorEvgeny KalashnikovLead Software Engineer@Blackline, Adhoc Studio
Share

Comments

More from the blog

Released under the MIT License.