Watcher
Track changes to observable properties in MobX models.
Watcher is designed to detect whether something has changed - useful for dirty state tracking in forms, triggering side effects when any field changes, detecting modifications for auto-save or data synchronization, and tracking change history. The changedKeys and changedKeyPaths properties are primarily for debugging purposes to identify which fields changed.
Getting a Watcher Instance
Section titled “Getting a Watcher Instance”Use Watcher.get() to retrieve or create a watcher instance for an object:
const model = new MyModel();const watcher = Watcher.get(model);Watcher instances are cached. The same object always returns the same watcher instance:
const watcher1 = Watcher.get(model);const watcher2 = Watcher.get(model);// watcher1 === watcher2 (same instance)A watcher is garbage collected together with its target, even when the target references state that outlives it, such as a @nested object or an observable array, set or map that other objects share. The exception is a @computed of the target that the watcher observes, such as a @watch @computed, while it reads such state, like a store: as long as the watcher observes the computed, the computed observes the store, and a computed references the object it belongs to, so the store keeps the target alive. MobX alone would let the target go, as a computed that nothing observes does not observe what it reads. A watcher cannot be disposed; to let such a target go, take @watch off, which also stops tracking the property's changes.
Use Watcher.getSafe() to get a watcher without throwing errors for non-objects:
const watcher = Watcher.getSafe(maybeObject);// Returns null if maybeObject is not an objectWatcher State Properties
Section titled “Watcher State Properties”Access the current change state through these reactive properties:
// Boolean indicating any changeswatcher.changed // boolean
// Set of changed property names (direct properties only)// e.g., "name", "age"watcher.changedKeys // Set<KeyPath>
// All changed paths including nested (when using @nested)// "child.value", "items.0.name"watcher.changedKeyPaths // Set<KeyPath>
// Counter that increments with each change (useful for reactions)watcher.changedTick // bigintStarting a Watcher
Section titled “Starting a Watcher”Watching starts immediately when the Watcher instance is created.
The watcher begins tracking changes as soon as Watcher.get() is called for the first time, including the changes made after it in the same transaction.
To set a new starting point after initialization, use the reset() method. For fine-grained control over what gets tracked, see Temporarily Disable Tracking.
const model = new Model();
runInAction(() => { model.name = "John"; // Not tracked: Watcher instance hasn't been created yet.});
const watcher = Watcher.get(model); // Starts tracking from this point forwardwatcher.changed // false
runInAction(() => { model.name = "Not John"; // Tracked});
watcher.changed // trueBasic Change Tracking
Section titled “Basic Change Tracking”@observable is automatically tracked unless explicitly excluded with @unwatch. @computed is not: see Computed properties.
- Properties are tracked using shallow comparison by default (arrays/sets/maps are compared by creating shallow copies)
- Use
@watch.reffor identity comparison only (reference equality) - The
changedTickproperty usesbigint(starts at0n) and can track unlimited changes
Scalar properties
Section titled “Scalar properties”class Model { @observable name = ""; @observable age = 0;
constructor() { makeObservable(this); }}
const model = new Model();const watcher = Watcher.get(model);
runInAction(() => { model.name = "John";});
watcher.changed // truewatcher.changedKeys // Set(["name"])Object properties
Section titled “Object properties”Assignments to the property are tracked, but changes to nested properties are NOT.
See also: Tracking Nested Objects.
class Model { @observable user = { name: "John" };
constructor() { makeObservable(this); }}
const model = new Model();const watcher = Watcher.get(model);
runInAction(() => { model.user.name = "Jane"; // Not tracked});
watcher.changed // false
runInAction(() => { model.user = { name: "Jane" }; // Tracked - assignment to property});
watcher.changed // truewatcher.changedKeys // Set(["user"])Arrays, Sets, and Maps
Section titled “Arrays, Sets, and Maps”Mutations are tracked, but element changes are NOT.
See also: Tracking Nested Objects.
class Model { @observable items = [{ value: 1 }]; @observable tags = new Set(["a"]); @observable data = new Map([["key", { value: 1 }]]);
constructor() { makeObservable(this); }}
const model = new Model();const watcher = Watcher.get(model);
// Tracked: mutations to the collection itselfrunInAction(() => { model.items.push({ value: 2 }); model.tags.add("b"); model.data.set("key2", { value: 2 });});watcher.changedKeys // Set(["items", "tags", "data"])
watcher.reset();
// NOT tracked: changes to elementsrunInAction(() => { model.items[0].value = 99; model.data.get("key")!.value = 99;});watcher.changed // falseComputed properties
Section titled “Computed properties”@computed is not tracked unless you add @watch (or @watch.ref). A computed value derives from other state, and a change to that state is detected where the state is tracked. Tracking the computed too would report the same edit twice, and would report changes to state the model doesn't own, like a store the computed reads.
class Model { @observable name = ""; @observable age = 0;
constructor() { makeObservable(this); }
@computed get displayName() { return `${this.name} (${this.age})`; }
@watch @computed get isAdult() { return this.age >= 18; }}
const model = new Model();const watcher = Watcher.get(model);
runInAction(() => { model.name = "John"; model.age = 20;});
watcher.changedKeys // Set(["name", "age", "isAdult"]) - displayName is not trackedSo excluding a property with @unwatch excludes the computed values derived from it too, unless they are tracked with @watch. See @unwatch.