Observers

← Themes · Documentation · Next: Streaming →

Observers dispatch actions in response to state changes or action failures. They are declared in a widget's observers array.

State observer

{
  "id": "root",
  "type": "Column",
  "state": {
    "form": { "agreed": false }
  },
  "observers": [
    {
      "id": "acceptLegal",
      "watch": "$root.form.agreed",
      "trigger": "becomesTrue",
      "skipInitial": true,
      "debounceMs": 0,
      "once": false,
      "action": {
        "type": "http",
        "method": "POST",
        "url": "/v1/legal/accept",
        "success": { "apply": "none" }
      }
    }
  ],
  "children": []
}
FieldTypeDefaultMeaning
idstringabsentStable identity for once, diagnostics, and replacement
watchpathrequiredState path or $error
triggerstringchangedState comparison mode
equalsJSONabsentComparison value for equals
jsPredicatestringabsentSandboxed predicate for predicate
skipInitialbooleantrueSuppress first state snapshot
debounceMsinteger0Coalesce rapid changes
oncebooleanfalseDisable after first successful action dispatch
actionactionrequiredAction to dispatch

Triggers

changed

Structural JSON inequality:

{
  "watch": "$root.filters",
  "trigger": "changed",
  "debounceMs": 250,
  "action": {
    "type": "http",
    "method": "POST",
    "url": "/v1/search",
    "body": { "fromState": "$root.filters" },
    "success": {
      "apply": "patch",
      "from": "$response.ops"
    }
  }
}

equals

Fires when the new value structurally equals equals and the old value did not:

{
  "watch": "$root.upload.status",
  "trigger": "equals",
  "equals": "complete",
  "action": { "type": "toast", "message": "Upload complete" }
}

becomesTrue / becomesFalse

Boolean edge trigger:

{
  "watch": "$local.expired",
  "trigger": "becomesTrue",
  "once": true,
  "action": {
    "type": "dialog",
    "presentation": "alert",
    "content": {
      "type": "Column",
      "children": [
        { "type": "Text", "params": { "text": "Session expired", "variant": "titleMedium" } },
        {
          "type": "Button",
          "onClick": { "type": "dismiss" },
          "children": [{ "type": "Text", "params": { "text": "OK" } }]
        }
      ]
    }
  }
}

Only literal JSON booleans count; strings such as "true" do not.

predicate

Runs a safe synchronous predicate:

{
  "watch": "$root.form",
  "trigger": "predicate",
  "jsPredicate": "function predicate(currentJson, previousJson) { var c = JSON.parse(currentJson); return JSON.stringify(!!(c.email && c.agreed)); }",
  "action": {
    "type": "patch",
    "patch": [{ "op": "add", "path": "$root/state/canSubmit", "value": true }]
  }
}

It must declare predicate(currentJson, previousJson) and return a JSON boolean string. It uses the same isolate, policy, time, source, and payload limits as jsUpdateState. A disabled/failed predicate evaluates to false and is logged; it does not enter the action error pipeline because no action was dispatched.

Scope

An observer is evaluated in the node where it is declared:

  • $local is that node's own state;
  • $parent is its nearest state-owning ancestor;
  • $root and $id.* are absolute;
  • the observer's action inherits the declaring node as sourceWidgetId;
  • observer actions may use patch; $local / $parent in patch paths are the declaring widget and its nearest ancestor widget.
{
  "id": "offer-42",
  "type": "Card",
  "state": { "liked": false },
  "observers": [
    {
      "watch": "$local.liked",
      "trigger": "changed",
      "action": {
        "type": "analytics",
        "event": "offer_like_changed",
        "params": {
          "offerId": "$local.id",
          "liked": "$local.liked"
        }
      }
    }
  ],
  "children": []
}

Scheduling and loop safety

State observers run after a dispatch commit/group, not during Compose recomposition.

  1. Commit store snapshot.
  2. Compare each watched value with its previous value.
  3. Apply trigger and debounce.
  4. Queue matching observer actions.

Rules:

  • structural equality prevents same-value loops;
  • observer chains are capped at depth 3 per original dispatch;
  • excess events are dropped and logged;
  • a sync group produces one state-observer evaluation at completion/failure;
  • each completed async child may produce its own evaluation;
  • onChange automatic write-back occurs before observer evaluation;
  • restore/hydration is an initial snapshot, so skipInitial: true prevents process-restore side effects.

Error observers

watch: "$error" is the general action-error handler. It does not observe state and ignores state-trigger fields.

{
  "id": "root",
  "type": "Column",
  "observers": [
    {
      "id": "handleAllErrors",
      "watch": "$error",
      "action": {
        "type": "async",
        "actions": [
          {
            "type": "toast",
            "message": "$error.message",
            "style": "error"
          },
          {
            "type": "analytics",
            "event": "bdui_action_failed",
            "params": {
              "code": "$error.code",
              "actionType": "$error.actionType",
              "source": "$error.source"
            }
          }
        ]
      }
    }
  ],
  "children": []
}

Scope:

  • root error observer sees every failure in this view;
  • non-root observer sees failures dispatched by that node or descendants;
  • failures without a sourceWidgetId (for example SSE) reach root only.

Order:

  1. HTTP error-body apply, if any;
  2. failed action's optional onError;
  3. matching error observers from nearest node outward to root;
  4. default UI toast only if no onError or error observer ran.

$error is ephemeral context, never written to state. A failing error handler/observer is logged and cannot recursively trigger error observers.

Use the root error observer for consistent product-wide reporting in that view. Use per-action onError for rollback, cleanup, or a message that requires action-specific context.

← Themes · Documentation · Next: Streaming →

AppSana — экраны под задачу в приложениях, которые у вас уже есть.