Skip to main content

react

React renderer for json-render that turns JSON specs into React components. Use when working with @json-render/react, building React UIs from JSON, creating component catalogs, or rendering AI-generated specs.

Source facts

Repository
vercel-labs/json-render
Last source activity
August 15, 2026 at 01:22
Detected SKILL.md language
English
Stars
18,614
Forks
987

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
react
description
React renderer for json-render that turns JSON specs into React components. Use when working with @json-render/react, building React UIs from JSON, creating component catalogs, or rendering AI-generated specs.
# @json-render/react React renderer that converts JSON specs into React component trees. ## Quick Start ```typescript 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} />; } ``` ## Creating a Catalog ```typescript 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() }), slots: ["default"], description: "Card container with title", }, Layout: { props: z.object({}), slots: ["default", "header", "footer"], description: "Layout with named content regions", }, }, }); // 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> ), Layout: ({ children, slots }) => ( <div> <header>{slots?.header}</header> <main>{children}</main> <footer>{slots?.footer}</footer> </div> ), }, }); ``` ## Spec Structure (Element Tree) The React schema uses an element tree format: ```json { "root": { "type": "Card", "props": { "title": "Hello" }, "children": [{ "type": "Button", "props": { "label": "Click me" } }] } } ``` ## Named Slots Use `children` for the `"default"` slot. Use the element's top-level `slots` object for other slot names declared by the catalog: ```json { "type": "Layout", "props": {}, "children": ["main"], "slots": { "header": ["heading"], "footer": ["actions"] } } ``` Registry components receive named content as `slots?.header`, `slots?.footer`, and so on. Do not use `slots.default`. ## 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.): ```tsx 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. - **Filtered lists**: `repeat` plus an `$item` visible condition on the same container renders only matching items: `{ "repeat": { "statePath": "/tasks", "key": "id" }, "visible": { "$item": "status", "eq": "todo" }, "children": ["task-card"] }`. AND-composed `$state` conjuncts gate the container shell; `$item`/`$index` conjuncts filter items. - **Nested lists**: inside a repeat, use `{ "repeat": { "statePath": { "$item": "comments" }, "key": "id" } }` to iterate an array on the enclosing item. - **`{ "$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 ```json { "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`: ```tsx <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: ```tsx // 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> ); }, ``` ```json { "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: ```json { "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`: ```json { "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" }`: ```tsx 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`): ```typescript import type { BaseComponentProps } from "@json-render/react"; const Card = ({ props, children }: BaseComponentProps<{ title?: string }>) => (
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub