json-render-react

Convert JSON specifications into type-safe React component trees with state management and dynamic expressions. Supports declarative UI specs with element trees, two-way data binding via $bindState , conditional rendering, and computed expressions Built-in state management through StateProvider with optional external store integration (Redux, Zustand, XState) and JSON Pointer-based state paths Event system with action dispatching, state watchers, and four built-in actions: setState , pushState , removeState , and validateForm Type-safe catalog definition using Zod schemas for component props, with useBoundProp hook for form components and visibility helpers for conditional UI

INSTALLATION
npx skills add https://github.com/vercel-labs/json-render --skill json-render-react
Run in your project or agent environment. Adjust flags if your CLI version differs.

SKILL.md

$2a

function App({ spec }) {

return ;

}

## Creating a Catalog

import { defineCatalog } from "@json-render/core";

import { schema } from "@json-render/react/schema";

import { 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>

),

},

});


## Spec Structure (Element Tree)

The React schema uses an element tree format:

{

"root": {

"type": "Card",

"props": { "title": "Hello" },

"children": [

{ "type": "Button", "props": { "label": "Click me" } }

]

}

}


## Visibility Conditions

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)`.

## Providers

Provider
Purpose

`StateProvider`
Share state across components (JSON Pointer paths). Accepts optional `store` prop for controlled mode.

`ActionProvider`
Handle actions dispatched via the event system

`VisibilityProvider`
Enable conditional rendering based on state

`ValidationProvider`
Form field validation

### External Store (Controlled Mode)

Pass a `StateStore` to `StateProvider` (or `JSONUIProvider` / `createRenderer`) to use external state management (Redux, Zustand, XState, etc.):

import { createStateStore, type StateStore } from "@json-render/react";

const store = createStateStore({ count: 0 });

<StateProvider store={store}>{children}</StateProvider>

// Mutate from anywhere — React re-renders automatically:

store.set("/count", 1);


When `store` is provided, `initialState` and `onStateChange` are ignored.

## Dynamic Prop Expressions

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

- **`{ "$template": "Hello, ${/name}!" }`** - interpolates state values into strings

- **`{ "$computed": "fn", "args": { ... } }`** - calls registered functions with resolved args

{

"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.

Register `$computed` functions via the `functions` prop on `JSONUIProvider` or `createRenderer`:

<JSONUIProvider

functions={{ fullName: (args) => ${args.first} ${args.last} }}

>


## Event System

Components use `emit` to fire named events, or `on()` to get an event handle with metadata. The element's `on` field maps events to action bindings:

// Simple event firing

Button: ({ props, emit }) => (

<button onClick={() => emit("press")}>{props.label}</button>

),

// Event handle with metadata (e.g. preventDefault)

Link: ({ props, on }) => {

const click = on("click");

return (

<a href={props.href} onClick={(e) => {

if (click.shouldPreventDefault) e.preventDefault();

click.emit();

}}>{props.label}</a>

);

},

{

"type": "Button",

"props": { "label": "Submit" },

"on": { "press": { "action": "submit" } }

}


The `EventHandle` returned by `on()` has: `emit()`, `shouldPreventDefault` (boolean), and `bound` (boolean).

## State Watchers

Elements can declare a `watch` field (top-level, sibling of type/props/children) to trigger actions when state values change:

{

"type": "Select",

"props": { "value": { "$bindState": "/form/country" }, "options": ["US", "Canada"] },

"watch": { "/form/country": { "action": "loadCities" } },

"children": []

}


## Built-in Actions

The `setState`, `pushState`, `removeState`, and `validateForm` actions are built into the React schema and handled automatically by `ActionProvider`. They are injected into AI prompts without needing to be declared in catalog `actions`:

{ "action": "setState", "params": { "statePath": "/activeTab", "value": "home" } }

{ "action": "pushState", "params": { "statePath": "/items", "value": { "text": "New" } } }

{ "action": "removeState", "params": { "statePath": "/items", "index": 0 } }

{ "action": "validateForm", "params": { "statePath": "/formResult" } }


`validateForm` validates all registered fields and writes `{ valid, errors }` to state.

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`.

## useBoundProp

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).

## BaseComponentProps

For building reusable component libraries not tied to a specific catalog (e.g. `@json-render/shadcn`):

import type { BaseComponentProps } from "@json-render/react";

const Card = ({ props, children }: BaseComponentProps<{ title?: string }>) => (

<div>{props.title}{children}</div>

);

BrowserAct

Let your agent run on any real-world website

Bypass CAPTCHA & anti-bot for free. Start local, scale to cloud.

Explore BrowserAct Skills →

Stop writing automation&scrapers

Install the CLI. Run your first Skill in 30 seconds. Scale when you're ready.

Start free
free · no credit card