@json-render/solid
@json-render/solid renders json-render specs into Solid component trees with fine-grained reactivity.
Quick Start
tsximport { Renderer, JSONUIProvider } from "@json-render/solid"; import type { Spec } from "@json-render/solid"; import { registry } from "./registry"; export function App(props: { spec: Spec | null }) { return ( <JSONUIProvider registry={registry} initialState={{}}> <Renderer spec={props.spec} registry={registry} /> </JSONUIProvider> ); }
Create a Catalog
typescriptimport { defineCatalog } from "@json-render/core"; import { schema } from "@json-render/solid/schema"; import { z } from "zod"; 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", }, }, actions: { submit: { description: "Submit data" }, }, });
Define Components
Components receive ComponentRenderProps from the renderer:
tsinterface ComponentRenderProps<P = Record<string, unknown>> { element: UIElement<string, P>; children?: JSX.Element; emit: (event: string) => void; on: (event: string) => EventHandle; bindings?: Record<string, string>; loading?: boolean; }
Example:
tsximport type { BaseComponentProps } from "@json-render/solid"; export function Button(props: BaseComponentProps<{ label: string }>) { return ( <button onClick={() => props.emit("press")}>{props.props.label}</button> ); }
Create a Registry
typescriptimport { defineRegistry } from "@json-render/solid"; import { catalog } from "./catalog"; import { Card } from "./Card"; import { Button } from "./Button"; const { registry, handlers, executeAction } = defineRegistry(catalog, { components: { Card, Button, }, actions: { submit: async (params, setState, state) => { // custom action logic }, }, });
Spec Structure
json{ "root": "card1", "elements": { "card1": { "type": "Card", "props": { "title": "Hello" }, "children": ["btn1"] }, "btn1": { "type": "Button", "props": { "label": "Click me" }, "on": { "press": { "action": "submit" } } } } }
Providers
StateProvider: state model read/write and controlled mode viastoreVisibilityProvider: evaluatesvisibleconditionsValidationProvider: field validation +validateFormintegrationActionProvider: runs built-in and custom actionsJSONUIProvider: combined provider wrapper
Hooks
useStateStore,useStateValue,useStateBindinguseVisibility,useIsVisibleuseActions,useActionuseValidation,useOptionalValidation,useFieldValidationuseBoundPropuseUIStream,useChatUI
Built-in Actions
Handled automatically by ActionProvider:
setStatepushStateremoveStatevalidateForm
Dynamic Props and Bindings
Nested lists can set repeat.statePath to { "$item": "field" } inside an enclosing repeat.
Supported expression forms include:
{"$state": "/path"}{"$bindState": "/path"}{"$bindItem": "field"}{"$template": "Hi ${/user/name}"}{"$computed": "fn", "args": {...}}{"$cond": <condition>, "$then": <value>, "$else": <value>}
Use useBoundProp in components for writable bound values:
tsximport { useBoundProp } from "@json-render/solid"; function Input(props: BaseComponentProps<{ value?: string }>) { const [value, setValue] = useBoundProp( props.props.value, props.bindings?.value, ); return ( <input value={String(value() ?? "")} onInput={(e) => setValue(e.currentTarget.value)} /> ); }
useStateValue, useStateBinding, and the state / errors / isValid fields from useFieldValidation are reactive accessors in Solid. Call them as functions inside JSX, createMemo, or createEffect.
Solid Reactivity Rules
- Do not destructure component props in function signatures when values need to stay reactive.
- Keep changing reads inside JSX expressions,
createMemo, orcreateEffect. - Context values are exposed through getter-based objects so consumers always observe live signals.
Streaming UI
tsximport { useUIStream, Renderer } from "@json-render/solid"; const stream = useUIStream({ api: "/api/generate-ui" }); await stream.send("Create a support dashboard"); <Renderer spec={stream.spec} registry={registry} loading={stream.isStreaming} />;
Use useChatUI for chat + UI generation flows.

