json-render-react by vercel-labs/json-render
npx skills add https://github.com/vercel-labs/json-render --skill json-render-react将 JSON 规范转换为 React 组件树的渲染器。
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 } from "@json-render/react/schema";
import { defineRegistry } from "@json-render/react";
import { z } from "zod";
// 使用属性模式创建目录
export const catalog = defineCatalog(schema, {
components: {
Button: {
props: z.object({
label: z.string(),
variant: z.enum(["primary", "secondary"]).nullable(),
}),
description: "可点击按钮",
},
Card: {
props: z.object({ title: z.string() }),
description: "带标题的卡片容器",
},
},
});
// 定义具有类型安全属性的组件实现
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>
),
},
});
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
React 模式使用元素树格式:
{
"root": {
"type": "Card",
"props": { "title": "Hello" },
"children": [
{ "type": "Button", "props": { "label": "Click me" } }
]
}
}
在元素上使用 visible 来根据状态显示/隐藏。新语法:{ "$state": "/path" }、{ "$state": "/path", "eq": value }、{ "$state": "/path", "not": true }、{ "$and": [cond1, cond2] } 用于 AND 逻辑、{ "$or": [cond1, cond2] } 用于 OR 逻辑。辅助函数:visibility.when("/path")、visibility.unless("/path")、visibility.eq("/path", val)、visibility.and(cond1, cond2)、visibility.or(cond1, cond2)。
| 提供者 | 用途 |
|---|---|
StateProvider | 跨组件共享状态(JSON Pointer 路径)。接受可选的 store 属性用于受控模式。 |
ActionProvider | 处理通过事件系统分发的操作 |
VisibilityProvider | 启用基于状态的条件渲染 |
ValidationProvider | 表单字段验证 |
将 StateStore 传递给 StateProvider(或 JSONUIProvider / createRenderer)以使用外部状态管理(Redux、Zustand、XState 等):
import { createStateStore, type StateStore } from "@json-render/react";
const store = createStateStore({ count: 0 });
<StateProvider store={store}>{children}</StateProvider>
// 从任何地方进行变更 —— React 会自动重新渲染:
store.set("/count", 1);
当提供了 store 时,initialState 和 onStateChange 将被忽略。
任何属性值都可以是数据驱动的表达式,在组件接收属性之前由渲染器解析:
{ "$state": "/state/key" } - 从状态模型中读取(单向读取){ "$bindState": "/path" } - 双向绑定:从状态读取并支持写回。用于表单组件的自然值属性(value、checked、pressed 等)。{ "$bindItem": "field" } - 与重复项字段的双向绑定。在重复作用域内使用。{ "$cond":<condition>, "$then": <value>, "$else": <value> } - 条件值{ "$template": "Hello, ${/name}!" } - 将状态值插入字符串{ "$computed": "fn", "args": { ... } } - 使用解析后的参数调用注册的函数{
"type": "Input",
"props": {
"value": { "$bindState": "/form/email" },
"placeholder": "Email"
}
}
组件不使用 statePath 属性进行双向绑定。请改为在自然值属性上使用 { "$bindState": "/path" }。
组件接收已解析的属性。对于双向绑定的属性,请使用 useBoundProp 钩子与渲染器提供的 bindings 映射。
通过 JSONUIProvider 或 createRenderer 上的 functions 属性注册 $computed 函数:
<JSONUIProvider
functions={{ fullName: (args) => `${args.first} ${args.last}` }}
>
组件使用 emit 来触发命名事件,或使用 on() 来获取带有元数据的事件句柄。元素的 on 字段将事件映射到操作绑定:
// 简单事件触发
Button: ({ props, emit }) => (
<button onClick={() => emit("press")}>{props.label}</button>
),
// 带有元数据的事件句柄(例如 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" } }
}
on() 返回的 EventHandle 包含:emit()、shouldPreventDefault(布尔值)和 bound(布尔值)。
元素可以声明一个 watch 字段(顶层,与 type/props/children 同级),以便在状态值更改时触发操作:
{
"type": "Select",
"props": { "value": { "$bindState": "/form/country" }, "options": ["US", "Canada"] },
"watch": { "/form/country": { "action": "loadCities" } },
"children": []
}
setState、pushState、removeState 和 validateForm 操作内置于 React 模式中,并由 ActionProvider 自动处理。它们被注入到 AI 提示中,无需在目录 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 验证所有已注册的字段,并将 { valid, errors } 写入状态。
注意:操作参数中的 statePath(例如 setState.statePath)指向变更路径。组件属性中的双向绑定在值属性上使用 { "$bindState": "/path" },而不是 statePath。
对于需要双向绑定的表单组件,当属性使用 { "$bindState": "/path" } 或 { "$bindItem": "field" } 时,请使用 useBoundProp 与渲染器提供的 bindings 映射:
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) 返回 [value, setValue]。value 是解析后的属性;setValue 写回绑定的状态路径(如果未绑定则为空操作)。
用于构建不绑定到特定目录的可重用组件库(例如 @json-render/shadcn):
import type { BaseComponentProps } from "@json-render/react";
const Card = ({ props, children }: BaseComponentProps<{ title?: string }>) => (
<div>{props.title}{children}</div>
);
defineRegistry 仅在目录声明了操作时才需要有条件地要求 actions 字段。具有 actions: {} 的目录可以省略它。
| 导出项 | 用途 |
|---|---|
defineRegistry | 从目录创建类型安全的组件注册表 |
Renderer | 使用注册表渲染规范 |
schema | 元素树模式(包含内置状态操作:setState、pushState、removeState、validateForm) |
useStateStore | 访问状态上下文 |
useStateValue | 从状态获取单个值 |
useBoundProp | 用于 $bindState/$bindItem 表达式的双向绑定 |
useActions | 访问操作上下文 |
useAction | 获取单个操作分发函数 |
useOptionalValidation | useValidation 的非抛出变体(如果没有提供者则返回 null) |
useUIStream | 从 API 端点流式传输规范 |
createStateStore | 创建与框架无关的内存 StateStore |
StateStore | 用于接入外部状态管理的接口 |
BaseComponentProps | 用于可重用组件库的与目录无关的基础类型 |
EventHandle | 事件句柄类型(emit、shouldPreventDefault、bound) |
ComponentContext | 类型化的组件上下文(感知目录) |
每周安装量
391
代码仓库
GitHub 星标数
13.3K
首次出现
2026年2月5日
安全审计
安装于
opencode366
codex362
gemini-cli360
github-copilot356
kimi-cli334
amp334
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 } 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>
),
},
});
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). 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 |
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.
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
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}` }}
>
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).
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": []
}
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.
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).
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>
);
defineRegistry conditionally requires the actions field only when the catalog declares actions. Catalogs with actions: {} can omit it.
| 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 state actions: setState, pushState, removeState, validateForm) |
useStateStore | Access state context |
useStateValue | Get single value from state |
useBoundProp |
Weekly Installs
391
Repository
GitHub Stars
13.3K
First Seen
Feb 5, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
opencode366
codex362
gemini-cli360
github-copilot356
kimi-cli334
amp334
React 组合模式指南:Vercel 组件架构最佳实践,提升代码可维护性
105,000 周安装
Rust调用关系图生成器 - 可视化函数调用层次结构,提升代码分析效率
539 周安装
parallel-web-extract:并行网页内容提取工具,高效抓取网页数据
595 周安装
腾讯云CloudBase AI模型Web技能:前端调用混元/DeepSeek模型,实现流式文本生成
560 周安装
Apollo Connectors 模式助手:GraphQL API 连接器开发与集成指南
565 周安装
GitHub Trending 趋势分析工具:实时发现热门项目、技术洞察与开源机会
556 周安装
GSAP React 集成教程:useGSAP Hook 动画库与 React 组件开发指南
546 周安装
{ "type": "Input", "props": { "value": { "$bindState": "/form/email" }, "placeholder": "Email" } }
Two-way binding for $bindState/$bindItem expressions |
useActions | Access actions context |
useAction | Get a single action dispatch function |
useOptionalValidation | Non-throwing variant of useValidation (returns null if no provider) |
useUIStream | Stream specs from an API endpoint |
createStateStore | Create a framework-agnostic in-memory StateStore |
StateStore | Interface for plugging in external state management |
BaseComponentProps | Catalog-agnostic base type for reusable component libraries |
EventHandle | Event handle type (emit, shouldPreventDefault, bound) |
ComponentContext | Typed component context (catalog-aware) |