mirror of
https://github.com/prdlk/dotfiles.git
synced 2026-11-03 03:37:36 +00:00
265 lines
9.1 KiB
Markdown
265 lines
9.1 KiB
Markdown
---
|
|||
|
|
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() }),
|
||
|
|
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:
|
||
|
|
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"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.):
|
||
|
|
|
||
|
|
```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.
|
||
|
|
- **`{ "$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 }>) => (
|
||
|
|
<div>{props.title}{children}</div>
|
||
|
|
);
|
||
|
|
```
|
||
|
|
|
||
|
|
## defineRegistry
|
||
|
|
|
||
|
|
`defineRegistry` conditionally requires the `actions` field only when the catalog declares actions. Catalogs with `actions: {}` can omit it.
|
||
|
|
|
||
|
|
## 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 state actions: setState, pushState, removeState, validateForm) |
|
||
|
|
| `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 |
|
||
|
|
| `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) |
|