# @json-render/react React renderer for json-render. Turn JSON specs into React components with data binding, visibility, and actions. ## Installation ```bash npm install @json-render/react @json-render/core zod ``` ## Quick Start ### 1. Create a Catalog ```typescript import { defineCatalog } from "@json-render/core"; import { schema } from "@json-render/react/schema"; import { z } from "zod"; export const catalog = defineCatalog(schema, { components: { Card: { props: z.object({ title: z.string(), description: z.string().nullable(), }), description: "A card container", }, Button: { props: z.object({ label: z.string(), action: z.string(), }), description: "A clickable button", }, Input: { props: z.object({ value: z.union([z.string(), z.record(z.unknown())]).nullable(), label: z.string(), placeholder: z.string().nullable(), }), description: "Text input field with optional value binding", }, }, actions: { submit: { description: "Submit the form" }, cancel: { description: "Cancel and close" }, }, }); ``` ### 2. Define Component Implementations `defineRegistry` conditionally requires the `actions` field only when the catalog declares actions. Catalogs with `actions: {}` can omit it entirely. ```tsx import { defineRegistry, useBoundProp } from "@json-render/react"; import { catalog } from "./catalog"; export const { registry } = defineRegistry(catalog, { components: { Card: ({ props, children }) => (

{props.title}

{props.description &&

{props.description}

} {children}
), Button: ({ props, emit }) => ( ), Input: ({ props, bindings }) => { const [value, setValue] = useBoundProp(props.value, bindings?.value); return ( ); }, }, }); ``` ### 3. Render Specs ```tsx import { Renderer, StateProvider, ActionProvider } from "@json-render/react"; import { registry } from "./registry"; function App({ spec }) { return ( console.log("Submit"), }}> ); } ``` ## Spec Format The React renderer uses a flat element map format: ```typescript interface Spec { root: string; // Key of the root element elements: Record; // Flat map of elements by key state?: Record; // Optional initial state } interface UIElement { type: string; // Component name from catalog props: Record; // Component props children?: string[]; // Keys of child elements visible?: VisibilityCondition; // Visibility condition } ``` Example spec: ```json { "root": "card-1", "elements": { "card-1": { "type": "Card", "props": { "title": "Welcome" }, "children": ["input-1", "btn-1"] }, "input-1": { "type": "Input", "props": { "value": { "$bindState": "/form/name" }, "label": "Name", "placeholder": "Enter name" } }, "btn-1": { "type": "Button", "props": { "label": "Submit" }, "children": [] } } } ``` ## Contexts ### StateProvider Share data across components with JSON Pointer paths: ```tsx {children} // In components: const { state, get, set } = useStateStore(); const name = get("/user/name"); // "John" set("/user/age", 25); ``` #### External Store (Controlled Mode) For full control over state, pass a `StateStore` to bypass the internal state and wire json-render to any state management library (Redux, Zustand, XState, etc.): ```tsx import { createStateStore, type StateStore } from "@json-render/react"; // Option 1: Use the built-in store outside of React const store = createStateStore({ count: 0 }); {children} // Mutate from anywhere — React will re-render automatically: store.set("/count", 1); // Option 2: Implement the StateStore interface with your own backend const zustandStore: StateStore = { get: (path) => getByPath(useStore.getState(), path), set: (path, value) => useStore.setState(prev => { /* ... */ }), update: (updates) => useStore.setState(prev => { /* ... */ }), getSnapshot: () => useStore.getState(), subscribe: (listener) => useStore.subscribe(listener), }; ``` When `store` is provided, `initialState` and `onStateChange` are ignored. The store is the single source of truth. The same `store` prop is available on `createRenderer`, `JSONUIProvider`, and `StateProvider`. ### ActionProvider Handle actions from components: ```tsx handleSubmit(params), cancel: () => handleCancel(), }} > {children} ``` ### VisibilityProvider Control element visibility based on data: ```tsx {children} // Elements can use visibility conditions: { "type": "Alert", "props": { "message": "Error!" }, "visible": { "$state": "/form/hasError" } } ``` ### ValidationProvider Add field validation: ```tsx {children} // Use validation hooks: const { errors, validate } = useFieldValidation("/form/email", { checks: [ { type: "required", message: "Email required" }, { type: "email", message: "Invalid email" }, ], }); ``` ## Hooks | Hook | Purpose | |------|---------| | `useStateStore()` | Access state context (`state`, `get`, `set`, `update`) | | `useStateValue(path)` | Get single value from state | | `useStateBinding(path)` | Two-way data binding (returns `[value, setValue]`) | | `useIsVisible(condition)` | Check if a visibility condition is met | | `useActions()` | Access action context | | `useAction(name)` | Get a single action dispatch function | | `useFieldValidation(path, config)` | Field validation state | | `useUIStream(options)` | Stream specs from an API endpoint | ## Visibility Conditions ```typescript // Truthiness check { "$state": "/user/isAdmin" } // Auth state (use state path) { "$state": "/auth/isSignedIn" } // Comparisons (flat style) { "$state": "/status", "eq": "active" } { "$state": "/count", "gt": 10 } // Negation { "$state": "/maintenance", "not": true } // Multiple conditions (implicit AND) [ { "$state": "/feature/enabled" }, { "$state": "/maintenance", "not": true } ] // Always / never true // always visible false // never visible ``` TypeScript helpers from `@json-render/core`: ```typescript import { visibility } from "@json-render/core"; visibility.when("/path") // { $state: "/path" } visibility.unless("/path") // { $state: "/path", not: true } visibility.eq("/path", val) // { $state: "/path", eq: val } visibility.neq("/path", val) // { $state: "/path", neq: val } visibility.and(cond1, cond2) // { $and: [cond1, cond2] } visibility.always // true visibility.never // false ``` ## Dynamic Prop Expressions Any prop value can use data-driven expressions that resolve at render time. The renderer resolves these transparently before passing props to components. ```json { "type": "Badge", "props": { "label": { "$state": "/user/role" }, "color": { "$cond": { "$state": "/user/role", "eq": "admin" }, "$then": "red", "$else": "gray" } } } ``` For two-way binding, use `{ "$bindState": "/path" }` on the natural value prop (e.g. `value`, `checked`, `pressed`). Inside repeat scopes, use `{ "$bindItem": "field" }` instead. Components receive resolved `bindings` with the state path for each bound prop; use `useBoundProp(props.value, bindings?.value)` to get `[value, setValue]`. See [@json-render/core](../core/README.md) for full expression syntax. ## Built-in Actions The `setState`, `pushState`, and `removeState` actions are built into the React schema and handled automatically by `ActionProvider`. They are injected into AI prompts without needing to be declared in your catalog's `actions`. They update the state model, which triggers re-evaluation of visibility conditions and dynamic prop expressions: ```json { "type": "Button", "props": { "label": "Switch Tab" }, "on": { "press": { "action": "setState", "params": { "statePath": "/activeTab", "value": "settings" } } }, "children": [] } ``` ## Component Props When using `defineRegistry`, components receive these props: ```typescript interface ComponentContext

{ props: P; // Typed props from the catalog (expressions resolved) children?: React.ReactNode; // Rendered children emit: (event: string) => void; // Emit a named event (always defined) on: (event: string) => EventHandle; // Get event handle with metadata loading?: boolean; // Whether the parent is loading bindings?: Record; // State paths for $bindState/$bindItem expressions (e.g. bindings.value) } interface EventHandle { emit: () => void; // Fire the event shouldPreventDefault: boolean; // Whether any binding requested preventDefault bound: boolean; // Whether any handler is bound } ``` Use `emit("press")` for simple event firing. Use `on("click")` when you need to check metadata like `shouldPreventDefault` or `bound`: ```tsx Link: ({ props, on }) => { const click = on("click"); return ( { if (click.shouldPreventDefault) e.preventDefault(); click.emit(); }} > {props.label} ); }, ``` Use `bindings?.value`, `bindings?.checked`, etc. with `useBoundProp()` for two-way bound form components. ### `BaseComponentProps` For building reusable component libraries that are not tied to a specific catalog (e.g. `@json-render/shadcn`), use the catalog-agnostic `BaseComponentProps` type: ```typescript import type { BaseComponentProps } from "@json-render/react"; const Card = ({ props, children }: BaseComponentProps<{ title?: string }>) => (

{props.title}{children}
); ``` ## Generate AI Prompts ```typescript const systemPrompt = catalog.prompt(); // Returns detailed prompt with component/action descriptions ``` ## Full Example ```tsx import { defineCatalog } from "@json-render/core"; import { schema } from "@json-render/react/schema"; import { defineRegistry, Renderer } from "@json-render/react"; import { z } from "zod"; const catalog = defineCatalog(schema, { components: { Greeting: { props: z.object({ name: z.string() }), description: "Displays a greeting", }, }, actions: {}, }); const { registry } = defineRegistry(catalog, { components: { Greeting: ({ props }) =>

Hello, {props.name}!

, }, }); const spec = { root: "greeting-1", elements: { "greeting-1": { type: "Greeting", props: { name: "World" }, children: [], }, }, }; function App() { return ; } ``` ## Key Exports | Export | Purpose | |--------|---------| | `defineRegistry` | Create a type-safe component registry from a catalog | | `Renderer` | Render a spec using a registry | | `schema` | Element tree schema (includes built-in actions: `setState`, `pushState`, `removeState`) | | `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 | | `createStateStore` | Create a framework-agnostic in-memory `StateStore` | ### Types | Export | Purpose | |--------|---------| | `ComponentContext` | Typed component render function context (catalog-aware) | | `BaseComponentProps` | Catalog-agnostic base type for reusable component libraries | | `EventHandle` | Event handle with `emit()`, `shouldPreventDefault`, `bound` | | `ComponentFn` | Component render function type | | `SetState` | State setter type | | `StateModel` | State model type | | `StateStore` | Interface for plugging in external state management |