Watchers

React to state changes by triggering actions when watched paths update.

The watch Field

Elements can have an optional watch field that maps state paths to action bindings. When the value at a watched path changes, the bound actions fire automatically.

watch is a top-level field on the element (sibling of type, props, children) — not inside props.

{
  "type": "Select",
  "props": {
    "label": "Country",
    "value": { "$bindState": "/form/country" },
    "options": ["US", "Canada", "UK"]
  },
  "watch": {
    "/form/country": {
      "action": "loadCities",
      "params": { "country": { "$state": "/form/country" } }
    }
  },
  "children": []
}

When the user selects a different country, the loadCities action fires with the new country value. The action handler can fetch city data and update state, causing a dependent city Select to re-render with new options.

Cascading Selects

A common pattern is cascading dropdowns where selecting a value in one field loads options for another:

{
  "root": "form",
  "elements": {
    "form": {
      "type": "Stack",
      "props": { "direction": "vertical", "gap": "md" },
      "children": ["country-select", "city-select"]
    },
    "country-select": {
      "type": "Select",
      "props": {
        "label": "Country",
        "value": { "$bindState": "/form/country" },
        "options": ["US", "Canada", "UK"]
      },
      "watch": {
        "/form/country": [
          { "action": "loadCities", "params": { "country": { "$state": "/form/country" } } },
          { "action": "setState", "params": { "statePath": "/form/city", "value": "" } }
        ]
      },
      "children": []
    },
    "city-select": {
      "type": "Select",
      "props": {
        "label": "City",
        "value": { "$bindState": "/form/city" },
        "options": { "$state": "/availableCities" },
        "placeholder": "Select a city"
      },
      "children": []
    }
  },
  "state": {
    "form": { "country": "", "city": "" },
    "availableCities": []
  }
}

The watcher on country-select fires two actions when the country changes:

  1. loadCities — fetches and writes city options to /availableCities
  2. setState — resets the city selection

The city Select reads its options from { "$state": "/availableCities" }, so it automatically updates when the data is loaded.

Action Handler

const handlers = {
  loadCities: async (params) => {
    const cities = await fetchCities(params.country);
    // setState is called by the runtime to write the result
    return cities;
  },
};

Or with defineRegistry:

const { registry, handlers } = defineRegistry(catalog, {
  components: { /* ... */ },
  actions: {
    loadCities: async (params, setState) => {
      const response = await fetch(`/api/cities?country=${params.country}`);
      const cities = await response.json();
      setState('/availableCities', cities);
    },
  },
});

Multiple Watchers

An element can watch multiple state paths. Each path maps to one or more action bindings:

{
  "watch": {
    "/form/startDate": { "action": "validateDateRange" },
    "/form/endDate": { "action": "validateDateRange" },
    "/form/quantity": [
      { "action": "recalculateTotal" },
      { "action": "checkInventory", "params": { "qty": { "$state": "/form/quantity" } } }
    ]
  }
}

Behavior

  • Watchers only fire on value changes, not on the initial render
  • Comparison is by reference (===), not deep equality
  • Action params support the same expressions as event bindings ($state, $item, $index)
  • Multiple action bindings on the same path execute sequentially

When to Use watch vs on

MechanismTriggerUse Case
onUser interaction (press, change, blur)Button clicks, input changes, form submissions
watchState value change (any source)Cascading data, derived state, cross-field sync

Use on when reacting to direct user actions. Use watch when a state change (from any source — user input, action handler, or external store update) should trigger side effects.

Next