Prototyper UI

Dynamic Expressions

Data binding, conditional values, and string interpolation in Stream UI specs

Expressions are special objects in prop values that are resolved at render time. They connect your UI to state, repeat contexts, and computed logic. Instead of static values, any prop can contain an expression that dynamically resolves based on the current state.

Expression Types

Stream UI supports 8 expression types:

ExpressionSyntaxDirectionDescription
$state{ "$state": "/path" }ReadRead a value from global state
$item{ "$item": "field" }ReadRead a field from the current repeat item
$index{ "$index": true }ReadCurrent repeat index (zero-based)
$bindState{ "$bindState": "/path" }Read + WriteTwo-way binding to a state path
$bindItem{ "$bindItem": "field" }Read + WriteTwo-way binding to a repeat item field
$cond{ "$cond": ..., "$then": ..., "$else": ... }ReadConditional value selection
$computed{ "$computed": "fnName", "args": {} }ReadCall a registered function
$template{ "$template": "Hello ${/name}!" }ReadString interpolation

$state — Read from State

Read a value from the global state model using a JSON Pointer path:

{
  "type": "Text",
  "props": {
    "content": { "$state": "/user/name" }
  }
}

With state { "user": { "name": "Alice" } }, the content prop resolves to "Alice".

Nested paths work with standard JSON Pointer syntax:

{ "$state": "/settings/theme/mode" }
{ "$state": "/items/0/title" }

If the path does not exist, the expression resolves to undefined.

$item — Read from Repeat Item

Inside a repeat scope, read a field from the current array item:

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

If the repeat source is /todos and the current item is { "title": "Buy milk", "done": false }, this resolves to "Buy milk".

Use an empty string to get the entire item:

{ "$item": "" }

Dot-separated paths are supported for nested fields:

{ "$item": "address.city" }

$index — Current Repeat Index

Returns the zero-based index of the current item within a repeat scope:

{
  "type": "Badge",
  "props": {
    "label": { "$index": true }
  }
}

To use the index in a string, combine with $template and reference the index via $cond or concatenation — note that $template interpolates state paths, not the repeat index directly. For display strings like "Item #1", use $computed or build labels from $index:

{
  "type": "Text",
  "props": {
    "content": {
      "$computed": "formatIndex",
      "args": { "index": { "$index": true } }
    }
  }
}

$bindState — Two-Way State Binding

Creates a two-way binding between a prop and a state path. The prop reads the current value from state, and when the component updates the value (e.g. user types in an input), the state is automatically written back:

{
  "type": "Input",
  "props": {
    "label": "Email",
    "value": { "$bindState": "/form/email" }
  }
}

This is the primary mechanism for form inputs. The component receives both the current value and a callback to update it. No explicit on.change action is needed for basic value synchronization.

$bindState is equivalent to $state for reads. The difference is that the renderer also extracts the path into a bindings map, allowing the component wrapper to set up a write-back channel.

$bindItem — Two-Way Repeat Item Binding

The repeat-scoped equivalent of $bindState. Binds a prop to a field on the current repeat item:

{
  "type": "Checkbox",
  "props": {
    "checked": { "$bindItem": "done" }
  }
}

Inside a repeat over /todos, for the item at index 2, this resolves to the absolute state path /todos/2/done for writes, and reads the current value of that field.

$cond — Conditional Values

Select between two values based on a condition. The $cond field uses the same visibility condition system:

{
  "type": "Badge",
  "props": {
    "variant": {
      "$cond": { "$state": "/user/isAdmin" },
      "$then": "default",
      "$else": "secondary"
    }
  }
}

The condition is evaluated using the visibility engine. If it passes, $then is returned; otherwise $else (which defaults to undefined if omitted).

Both $then and $else can themselves be expressions, enabling nested conditionals:

{
  "$cond": { "$state": "/status", "eq": "error" },
  "$then": "destructive",
  "$else": {
    "$cond": { "$state": "/status", "eq": "success" },
    "$then": "default",
    "$else": "secondary"
  }
}

You can use comparison operators in the condition:

{
  "$cond": { "$state": "/cart/total", "gt": 100 },
  "$then": "Free shipping!",
  "$else": { "$template": "Add ${/remaining} more for free shipping" }
}

$computed — Custom Functions

Call a named function registered on the Renderer:

{
  "type": "Text",
  "props": {
    "content": {
      "$computed": "formatCurrency",
      "args": { "amount": { "$state": "/cart/total" }, "currency": "USD" }
    }
  }
}

Register the function via the functions prop:

<Renderer
  spec={spec}
  registry={prototyperComponents}
  functions={{
    formatCurrency: ({ amount, currency }) =>
      new Intl.NumberFormat("en-US", { style: "currency", currency }).format(amount as number),
    uppercase: ({ text }) => String(text).toUpperCase(),
  }}
/>

Arguments in args are themselves resolved as expressions before being passed to the function. This means you can use $state, $item, or any other expression inside args.

$template — String Interpolation

Interpolate state values into a template string. References use ${/json/pointer/path} syntax:

{
  "type": "Text",
  "props": {
    "content": { "$template": "Hello, ${/user/name}! You have ${/notifications/count} new messages." }
  }
}

With state { "user": { "name": "Alice" }, "notifications": { "count": 5 } }, this resolves to "Hello, Alice! You have 5 new messages.".

  • Paths must start with / (JSON Pointer format)
  • Missing values resolve to an empty string
  • Non-string values are coerced via String()

When to Use Each Expression

ScenarioExpressionExample
Display a state value$stateShow user's name in a heading
Bind a form input$bindStateInput value synced to state
Display repeat item data$itemShow each todo's title in a list
Edit repeat item data$bindItemCheckbox for each todo's "done" field
Show item number$index"Item #1", "Item #2", etc.
Toggle variant/style$condRed badge for errors, green for success
Format/compute values$computedCurrency formatting, date formatting
Build dynamic strings$template"Welcome back, Alice!"

Resolution Order

When the renderer encounters a prop value, it checks for expressions in this order:

  1. $state — read from global state
  2. $item — read from repeat item
  3. $index — return repeat index
  4. $bindState — read from global state (also extracts binding path)
  5. $bindItem — read from repeat item (also extracts binding path)
  6. $cond — evaluate condition and resolve $then or $else
  7. $computed — call registered function
  8. $template — interpolate string

Plain objects that do not match any expression pattern are recursively resolved (each value is checked for expressions). Arrays are resolved element-by-element. Primitives (string, number, boolean, null) pass through unchanged.

Next Steps

  • Visibility — Conditional rendering using the same condition system as $cond
  • Actions — Using expressions in action parameters
  • Spec Format — The full spec structure including repeat bindings

On this page