Data Binding

Connect UI elements to dynamic data using expressions in your JSON specs.

State Model

Every spec can include a state object that holds the data your UI reads from:

{
  "root": "greeting",
  "elements": {
    "greeting": {
      "type": "Text",
      "props": { "content": { "$state": "/user/name" } },
      "children": []
    }
  },
  "state": {
    "user": { "name": "Alice" }
  }
}

State can also be provided programmatically at runtime. In @json-render/react, this is done via StateProvider and hooks like useStateStore. See the React API reference for details.

JSON Pointer Paths

All paths in json-render follow JSON Pointer (RFC 6901). A path is a string of /-separated tokens starting from the root:

Given this state:
{
  "user": { "name": "Alice", "email": "alice@example.com" },
  "todos": [
    { "title": "Buy milk", "done": false },
    { "title": "Walk dog", "done": true }
  ]
}

"/user/name"      -> "Alice"
"/user/email"     -> "alice@example.com"
"/todos/0/title"  -> "Buy milk"
"/todos/1/done"   -> true

Expressions

Expressions are special objects you place in props to read dynamic values instead of hardcoding them. There are six expression types.

$state — Read from state

Use { "$state": "/path" } in any prop to read a value from the state model:

{
  "type": "Card",
  "props": {
    "title": { "$state": "/user/name" },
    "subtitle": { "$state": "/user/email" }
  },
  "children": []
}

If state contains { "user": { "name": "Alice", "email": "alice@example.com" } }, the Card renders with title "Alice" and subtitle "alice@example.com".

$item — Read from the current repeat item

Use { "$item": "field" } inside a repeat to read a field from the current array item:

{
  "type": "Text",
  "props": { "content": { "$item": "title" } },
  "children": []
}

Use { "$item": "" } to get the entire item object.

$index — Current repeat index

Use { "$index": true } inside a repeat to get the current array index (zero-based number):

{
  "type": "Text",
  "props": { "content": { "$index": true } },
  "children": []
}

Repeat

The repeat field on an element renders its children once per item in a state array. It is a top-level field on the element, sibling of type, props, and children — not inside props.

{
  "root": "todo-list",
  "elements": {
    "todo-list": {
      "type": "Column",
      "props": { "gap": 8 },
      "repeat": { "statePath": "/todos", "key": "id" },
      "children": ["todo-item"]
    },
    "todo-item": {
      "type": "Card",
      "props": {
        "title": { "$item": "title" },
        "subtitle": { "$item": "description" }
      },
      "children": []
    }
  },
  "state": {
    "todos": [
      { "id": "1", "title": "Buy milk", "description": "2% or whole" },
      { "id": "2", "title": "Walk dog", "description": "Around the park" }
    ]
  }
}
  • repeat.statePath — JSON Pointer to the state array
  • repeat.key — field name on each item to use as a stable key for rendering

Inside todo-item, { "$item": "title" } reads the title field from whichever array item is currently being rendered. { "$index": true } would return 0 for the first item, 1 for the second, and so on.

Two-Way Binding with $bindState

Form components use { "$bindState": "/path" } on their natural value prop for two-way binding. The component reads from and writes to the state path.

Value prop (text inputs)

{
  "type": "TextInput",
  "props": {
    "value": { "$bindState": "/form/email" },
    "placeholder": "Enter your email"
  },
  "children": []
}

Checked prop (switches, checkboxes)

{
  "type": "Switch",
  "props": {
    "label": "Enable notifications",
    "checked": { "$bindState": "/settings/notifications" }
  },
  "children": []
}

Pressed prop (toggle buttons)

{
  "type": "ToggleButton",
  "props": {
    "label": "Bold",
    "pressed": { "$bindState": "/editor/bold" }
  },
  "children": []
}

Two-Way Binding with $bindItem

Inside a repeat scope, use { "$bindItem": "field" } to bind to a field on the current item:

{
  "type": "Switch",
  "props": {
    "label": "Done",
    "checked": { "$bindItem": "completed" }
  },
  "children": []
}

Use { "$bindItem": "" } to bind to the entire item.

statePath is not used for component binding. It remains for repeat.statePath (array iteration path) and action params like setState.statePath (target path for mutations).

Conditional Props

Use $cond / $then / $else to pick a prop value based on a condition:

{
  "type": "Badge",
  "props": {
    "label": {
      "$cond": { "$state": "/user/isAdmin" },
      "$then": "Admin",
      "$else": "Member"
    }
  },
  "children": []
}

The condition uses the same visibility expression format.

Quick Reference

ExpressionSyntaxContext
$state{ "$state": "/path" }Anywhere
$item{ "$item": "field" }Inside repeat only
$index{ "$index": true }Inside repeat only
$cond{ "$cond": ..., "$then": ..., "$else": ... }Anywhere
$bindState{ "$bindState": "/path" }Form components (value, checked, pressed)
$bindItem{ "$bindItem": "field" }Form components inside repeat

Next