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" -> trueExpressions#
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 arrayrepeat.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.
Template Strings#
Use { "$template": "..." } to interpolate state values into a string using ${/path} syntax:
{
"type": "Text",
"props": {
"text": { "$template": "Welcome back, ${/user/name}!" }
},
"children": []
}See Computed Values for details on $template and $computed expressions.
Quick Reference#
| Expression | Syntax | Context |
|---|---|---|
$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 |
$template | { "$template": "Hello, ${/name}!" } | Anywhere (string props) |
$computed | { "$computed": "fn", "args": { ... } } | Anywhere (requires registered function) |
External Store (Controlled Mode)#
For advanced use cases, you can pass a StateStore to StateProvider to use your own state management (Redux, Zustand, XState, etc.) instead of the built-in internal store:
import { createStateStore, type StateStore } from "@json-render/react";
const store = createStateStore({ user: { name: "Alice" } });
<StateProvider store={store}>
{children}
</StateProvider>
// Mutate from anywhere — React re-renders automatically:
store.set("/user/name", "Bob");When store is provided, initialState and onStateChange are ignored. The store is the single source of truth. See the React API reference for the full StateStore interface.
Next#
- Visibility — conditionally show or hide elements
- Action handlers — respond to user interactions
- React API reference — React-specific hooks for programmatic state access