name: json-render-react
React renderer that converts JSON specs into React component trees.
import { defineRegistry, Renderer } from "@json-render/react";
import { catalog } from "./catalog";
const { registry } = defineRegistry(catalog, {
components: {
Card: ({ props, children }) => <div>{props.title}{children}</div>,
},
});
function App({ spec }) {
return <Renderer spec={spec} registry={registry} />;
}
import { defineCatalog } from "@json-render/core";
import { schema, defineRegistry } from "@json-render/react";
import { z } from "zod";
// Create catalog with props schemas
export const catalog = defineCatalog(schema, {
components: {
Button: {
props: z.object({
label: z.string(),
variant: z.enum(["primary", "secondary"]).nullable(),
}),
description: "Clickable button",
},
Card: {
props: z.object({ title: z.string() }),
description: "Card container with title",
},
},
});
// Define component implementations with type-safe props
const { registry } = defineRegistry(catalog, {
components: {
Button: ({ props }) => (
<button className={props.variant}>{props.label}</button>
),
Card: ({ props, children }) => (
<div className="card">
<h2>{props.title}</h2>
{children}
</div>
),
},
});
The React schema uses an element tree format:
{
"root": {
"type": "Card",
"props": { "title": "Hello" },
"children": [
{ "type": "Button", "props": { "label": "Click me" } }
]
}
}
Use visible on elements to show/hide based on state. New syntax: { "$state": "/path" }, { "$state": "/path", "eq": value }, { "$state": "/path", "not": true }, { "$and": [cond1, cond2] } for AND, { "$or": [cond1, cond2] } for OR. Helpers: visibility.when("/path"), visibility.unless("/path"), visibility.eq("/path", val), visibility.and(cond1, cond2), visibility.or(cond1, cond2).
| Provider | Purpose |
|---|---|
StateProvider |
Share state across components (JSON Pointer paths) |
ActionProvider |
Handle actions dispatched via the event system |
VisibilityProvider |
Enable conditional rendering based on state |
ValidationProvider |
Form field validation |
Any prop value can be a data-driven expression resolved by the renderer before components receive props:
{ "$state": "/state/key" } - reads from state model (one-way read){ "$bindState": "/path" } - two-way binding: reads from state and enables write-back. Use on the natural value prop (value, checked, pressed, etc.) of form components.{ "$bindItem": "field" } - two-way binding to a repeat item field. Use inside repeat scopes.{ "$cond": <condition>, "$then": <value>, "$else": <value> } - conditional value
{
"type": "Input",
"props": {
"value": { "$bindState": "/form/email" },
"placeholder": "Email"
}
}
Components do not use a statePath prop for two-way binding. Use { "$bindState": "/path" } on the natural value prop instead.
Components receive already-resolved props. For two-way bound props, use the useBoundProp hook with the bindings map the renderer provides.
Components use emit to fire named events. The element's on field maps events to action bindings:
// Component emits a named event
Button: ({ props, emit }) => (
<button onClick={() => emit("press")}>{props.label}</button>
),
{
"type": "Button",
"props": { "label": "Submit" },
"on": { "press": { "action": "submit" } }
}
The setState action is handled automatically by ActionProvider and updates the state model directly, which re-evaluates visibility conditions and dynamic prop expressions:
{ "action": "setState", "actionParams": { "statePath": "/activeTab", "value": "home" } }
Note: statePath in action params (e.g. setState.statePath) targets the mutation path. Two-way binding in component props uses { "$bindState": "/path" } on the value prop, not statePath.
For form components that need two-way binding, use useBoundProp with the bindings map the renderer provides when a prop uses { "$bindState": "/path" } or { "$bindItem": "field" }:
import { useBoundProp } from "@json-render/react";
Input: ({ element, bindings }) => {
const [value, setValue] = useBoundProp<string>(
element.props.value,
bindings?.value
);
return (
<input
value={value ?? ""}
onChange={(e) => setValue(e.target.value)}
/>
);
},
useBoundProp(propValue, bindingPath) returns [value, setValue]. The value is the resolved prop; setValue writes back to the bound state path (no-op if not bound).
| Export | Purpose |
|---|---|
defineRegistry |
Create a type-safe component registry from a catalog |
Renderer |
Render a spec using a registry |
schema |
Element tree schema |
useStateStore |
Access state context |
useStateValue |
Get single value from state |
useBoundProp |
Two-way binding for $bindState/$bindItem expressions |
useActions |
Access actions context |
useAction |
Get a single action dispatch function |
useUIStream |
Stream specs from an API endpoint |