page.mdx 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. import { pageMetadata } from "@/lib/page-metadata"
  2. export const metadata = pageMetadata("docs/watchers")
  3. # Watchers
  4. React to state changes by triggering actions when watched paths update.
  5. ## The `watch` Field
  6. 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.
  7. `watch` is a **top-level field** on the element (sibling of `type`, `props`, `children`) — not inside `props`.
  8. ```json
  9. {
  10. "type": "Select",
  11. "props": {
  12. "label": "Country",
  13. "value": { "$bindState": "/form/country" },
  14. "options": ["US", "Canada", "UK"]
  15. },
  16. "watch": {
  17. "/form/country": {
  18. "action": "loadCities",
  19. "params": { "country": { "$state": "/form/country" } }
  20. }
  21. },
  22. "children": []
  23. }
  24. ```
  25. 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.
  26. ## Cascading Selects
  27. A common pattern is cascading dropdowns where selecting a value in one field loads options for another:
  28. ```json
  29. {
  30. "root": "form",
  31. "elements": {
  32. "form": {
  33. "type": "Stack",
  34. "props": { "direction": "vertical", "gap": "md" },
  35. "children": ["country-select", "city-select"]
  36. },
  37. "country-select": {
  38. "type": "Select",
  39. "props": {
  40. "label": "Country",
  41. "value": { "$bindState": "/form/country" },
  42. "options": ["US", "Canada", "UK"]
  43. },
  44. "watch": {
  45. "/form/country": [
  46. { "action": "loadCities", "params": { "country": { "$state": "/form/country" } } },
  47. { "action": "setState", "params": { "statePath": "/form/city", "value": "" } }
  48. ]
  49. },
  50. "children": []
  51. },
  52. "city-select": {
  53. "type": "Select",
  54. "props": {
  55. "label": "City",
  56. "value": { "$bindState": "/form/city" },
  57. "options": { "$state": "/availableCities" },
  58. "placeholder": "Select a city"
  59. },
  60. "children": []
  61. }
  62. },
  63. "state": {
  64. "form": { "country": "", "city": "" },
  65. "availableCities": []
  66. }
  67. }
  68. ```
  69. The watcher on `country-select` fires two actions when the country changes:
  70. 1. `loadCities` — fetches and writes city options to `/availableCities`
  71. 2. `setState` — resets the city selection
  72. The city Select reads its options from `{ "$state": "/availableCities" }`, so it automatically updates when the data is loaded.
  73. ### Action Handler
  74. ```typescript
  75. const handlers = {
  76. loadCities: async (params) => {
  77. const cities = await fetchCities(params.country);
  78. // setState is called by the runtime to write the result
  79. return cities;
  80. },
  81. };
  82. ```
  83. Or with `defineRegistry`:
  84. ```typescript
  85. const { registry, handlers } = defineRegistry(catalog, {
  86. components: { /* ... */ },
  87. actions: {
  88. loadCities: async (params, setState) => {
  89. const response = await fetch(`/api/cities?country=${params.country}`);
  90. const cities = await response.json();
  91. setState('/availableCities', cities);
  92. },
  93. },
  94. });
  95. ```
  96. ## Multiple Watchers
  97. An element can watch multiple state paths. Each path maps to one or more action bindings:
  98. ```json
  99. {
  100. "watch": {
  101. "/form/startDate": { "action": "validateDateRange" },
  102. "/form/endDate": { "action": "validateDateRange" },
  103. "/form/quantity": [
  104. { "action": "recalculateTotal" },
  105. { "action": "checkInventory", "params": { "qty": { "$state": "/form/quantity" } } }
  106. ]
  107. }
  108. }
  109. ```
  110. ## Behavior
  111. - Watchers only fire on **value changes**, not on the initial render
  112. - Comparison is by reference (`===`), not deep equality
  113. - Action params support the same expressions as event bindings (`$state`, `$item`, `$index`)
  114. - Multiple action bindings on the same path execute sequentially
  115. ## When to Use `watch` vs `on`
  116. <table>
  117. <thead>
  118. <tr>
  119. <th>Mechanism</th>
  120. <th>Trigger</th>
  121. <th>Use Case</th>
  122. </tr>
  123. </thead>
  124. <tbody>
  125. <tr>
  126. <td><code>on</code></td>
  127. <td>User interaction (press, change, blur)</td>
  128. <td>Button clicks, input changes, form submissions</td>
  129. </tr>
  130. <tr>
  131. <td><code>watch</code></td>
  132. <td>State value change (any source)</td>
  133. <td>Cascading data, derived state, cross-field sync</td>
  134. </tr>
  135. </tbody>
  136. </table>
  137. 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.
  138. ## Next
  139. - [Data Binding](/docs/data-binding) — connect elements to state
  140. - [Computed Values](/docs/computed-values) — derive prop values
  141. - [Visibility](/docs/visibility) — conditionally show or hide elements