Components
Studio writes this format for you. This page documents the underlying JSON — useful when you want to hand-edit a file, review a diff, or understand what the visual tools produce.
A Jx component is a single
.json
file. All state, computed values, and functions are declared in
state
. Simple components need no sidecar file.
Self-Describing Components
{
"$id": "Counter",
"state": {
"count": 0,
"increment": {
"$prototype": "Function",
"body": "state.count++"
}
},
"tagName": "my-counter",
"children": [
{ "tagName": "span", "textContent": "undefined" },
{ "tagName": "button", "textContent": "+", "onclick": { "$ref": "#/state/increment" } }
]
}
State Shapes
Every entry in
state
falls into one of four shapes, determinable by inspection:
Shape 1 — Naked Value
A JSON scalar, array, or plain object with no reserved keys:
{ "state": { "count": 0, "name": "World", "tags": [] } }
Shape 2 — Typed Value
An object with a
default
property and optional
type
:
{
"state": {
"count": {
"type": { "$ref": "#/$defs/Count" },
"default": 0,
"description": "Current counter value"
}
}
}
Shape 3 — Computed (Template String)
A string containing
syntax:
Shape 4 — Prototype (
$prototype
)
An object with
$prototype
for functions and data sources:
{
"state": {
"increment": {
"$prototype": "Function",
"body": "state.count++"
},
"userData": {
"$prototype": "Request",
"url": "/api/users/",
"method": "GET"
}
}
}
External Sidecars
When functions grow complex, extract them to a
.js
file:
{
"state": {
"increment": { "$prototype": "Function", "$src": "./counter.js" },
"decrement": { "$prototype": "Function", "$src": "./counter.js" }
}
}
export function increment(state) {
state.count++;
}
export function decrement(state) {
state.count = Math.max(0, state.count - 1);
}
The first parameter is always
state
— the component's reactive scope.
this
is never used.
Custom Elements
A component whose
tagName
contains a hyphen is a custom element:
{
"tagName": "user-card",
"state": {
"username": "Guest",
"status": "offline"
},
"children": [{ "tagName": "h3", "textContent": "undefined" }]
}
Custom elements render to the light DOM (no Shadow DOM). Style scoping uses
data-jx
attributes.
Props and Encapsulation
Props are passed via
$props
on an instance node — the only mechanism for crossing component boundaries. Register the component in
$elements
and instantiate it by its custom-element tag:
{
"$elements": { "my-card": { "$ref": "./card.json" } },
"children": [
{
"tagName": "my-card",
"$props": {
"title": "Static string",
"count": { "$ref": "#/state/count" }
}
}
]
}
Signal scope is bounded at the component level. No implicit scope leaking.