page.mdx 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330
  1. import { pageMetadata } from "@/lib/page-metadata"
  2. export const metadata = pageMetadata("docs/registry")
  3. # Registry
  4. A registry maps your [catalog](/docs/catalog) definitions to platform-specific implementations. The catalog defines *what* AI can generate — the registry provides the *how*.
  5. What a registry contains depends on the schema you use. Each package defines its own schema, which determines the shape of both the catalog and the registry.
  6. - **`@json-render/react`** — Components (React elements) and action handlers
  7. - **`@json-render/react-native`** — Components (React Native elements) and action handlers
  8. - **`@json-render/react-email`** — Email components (React Email / HTML)
  9. - **`@json-render/remotion`** — Clip components, transitions, and effects
  10. ## @json-render/react
  11. ### defineRegistry
  12. Use `defineRegistry` to create a type-safe registry from your catalog. Pass your components, actions, or both:
  13. ```tsx
  14. import { defineRegistry } from '@json-render/react';
  15. import { myCatalog } from './catalog';
  16. export const { registry, handlers, executeAction } = defineRegistry(myCatalog, {
  17. components: {
  18. Card: ({ props, children }) => (
  19. <div className="card">
  20. <h2>{props.title}</h2>
  21. {props.description && <p>{props.description}</p>}
  22. {children}
  23. </div>
  24. ),
  25. Button: ({ props, emit }) => (
  26. <button onClick={() => emit("press")}>
  27. {props.label}
  28. </button>
  29. ),
  30. },
  31. actions: {
  32. submit_form: async (params, setState) => {
  33. const res = await fetch('/api/submit', {
  34. method: 'POST',
  35. body: JSON.stringify(params),
  36. });
  37. const result = await res.json();
  38. setState((prev) => ({ ...prev, formResult: result }));
  39. },
  40. export_data: async (params) => {
  41. const blob = await generateExport(params.format);
  42. downloadBlob(blob, `export.${params.format}`);
  43. },
  44. },
  45. });
  46. ```
  47. The returned object contains:
  48. - `registry` — component registry for `<Renderer />`
  49. - `handlers` — factory for ActionProvider-compatible handlers
  50. - `executeAction` — imperative action dispatch (for use outside the React tree)
  51. ### Component Props
  52. Each component receives a `ComponentContext` object:
  53. ```typescript
  54. interface ComponentContext {
  55. props: T; // Type-safe props from your catalog
  56. children?: React.ReactNode; // Rendered children (for slot components)
  57. emit: (event: string) => void; // Emit a named event (always defined)
  58. on: (event: string) => EventHandle; // Get event handle with metadata
  59. loading?: boolean; // Whether the renderer is in a loading state
  60. bindings?: Record<string, string>; // State paths from $bindState/$bindItem expressions
  61. }
  62. interface EventHandle {
  63. emit: () => void; // Fire the event
  64. shouldPreventDefault: boolean; // Whether any binding requested preventDefault
  65. bound: boolean; // Whether any handler is bound
  66. }
  67. ```
  68. Props are automatically inferred from your catalog, so `props.title` is typed as `string` if your catalog defines it that way.
  69. Use `emit("press")` for simple event firing. Use `on("click")` when you need to inspect event metadata:
  70. ```tsx
  71. Link: ({ props, on }) => {
  72. const click = on("click");
  73. return (
  74. <a
  75. href={props.href}
  76. onClick={(e) => {
  77. if (click.shouldPreventDefault) e.preventDefault();
  78. click.emit();
  79. }}
  80. >
  81. {props.label}
  82. </a>
  83. );
  84. },
  85. ```
  86. #### Using `bindings` for two-way binding
  87. When a spec uses `{ "$bindState": "/path" }` or `{ "$bindItem": "field" }` on a prop, the renderer resolves the **value** into `props` and provides the **write-back path** in `bindings`. Use the `useBoundProp` hook to wire both together:
  88. ```tsx
  89. import { useBoundProp, defineRegistry } from '@json-render/react';
  90. // Inside your registry:
  91. TextInput: ({ props, bindings }) => {
  92. const [value, setValue] = useBoundProp<string>(props.value, bindings?.value);
  93. return (
  94. <input
  95. value={value ?? ""}
  96. onChange={(e) => setValue(e.target.value)}
  97. />
  98. );
  99. },
  100. ```
  101. `useBoundProp` returns `[resolvedValue, setter]`. The setter writes to the bound state path. If no binding exists (the prop is a literal), the setter is a no-op.
  102. ### Action Handlers
  103. Instead of AI generating arbitrary code, it declares *intent* by name. Your application provides the implementation. This is a core guardrail.
  104. Actions are declared in your [catalog](/docs/catalog). The `@json-render/react` schema supports an `actions` key where you define what operations AI can trigger:
  105. ```typescript
  106. import { defineCatalog } from '@json-render/core';
  107. import { schema } from '@json-render/react/schema';
  108. import { z } from 'zod';
  109. const catalog = defineCatalog(schema, {
  110. components: { /* ... */ },
  111. actions: {
  112. submit_form: {
  113. params: z.object({
  114. formId: z.string(),
  115. }),
  116. description: 'Submit a form',
  117. },
  118. export_data: {
  119. params: z.object({
  120. format: z.enum(['csv', 'pdf', 'json']),
  121. }),
  122. },
  123. navigate: {
  124. params: z.object({
  125. url: z.string(),
  126. }),
  127. },
  128. },
  129. });
  130. ```
  131. Action handlers receive `(params, setState, state)` and are defined inside `defineRegistry`:
  132. ```tsx
  133. export const { handlers, executeAction } = defineRegistry(catalog, {
  134. actions: {
  135. submit_form: async (params, setState) => {
  136. const response = await fetch('/api/submit', {
  137. method: 'POST',
  138. body: JSON.stringify({ formId: params.formId }),
  139. });
  140. const result = await response.json();
  141. setState((prev) => ({ ...prev, formResult: result }));
  142. },
  143. export_data: async (params) => {
  144. const blob = await generateExport(params.format);
  145. downloadBlob(blob, `export.${params.format}`);
  146. },
  147. navigate: (params) => {
  148. window.location.href = params.url;
  149. },
  150. },
  151. });
  152. ```
  153. ### Data Binding
  154. Most data binding is handled automatically by the renderer — `$state`, `$item`, and `$index` expressions in props are resolved before your component receives them. See the [Data Binding](/docs/data-binding) guide for the full reference.
  155. For two-way binding (form inputs), use `{ "$bindState": "/path" }` on the natural value prop (or `{ "$bindItem": "field" }` inside repeat scopes). The renderer provides a `bindings` map with the state path for each bound prop. Use `useBoundProp` to get `[value, setValue]`:
  156. ```tsx
  157. import { useBoundProp } from '@json-render/react';
  158. // Inside defineRegistry components:
  159. Input: ({ props, bindings }) => {
  160. const [value, setValue] = useBoundProp<string>(
  161. props.value,
  162. bindings?.value
  163. );
  164. return (
  165. <input
  166. value={value ?? ''}
  167. onChange={(e) => setValue(e.target.value)}
  168. placeholder={props.placeholder}
  169. />
  170. );
  171. },
  172. ```
  173. For read-only state access (e.g. displaying a value from state), use `$state` expressions in props — they are resolved before the component receives them. For custom logic, use `useStateStore` and `getByPath` from `@json-render/core`.
  174. ### Using the Renderer
  175. Wire everything together with providers and the `<Renderer />` component:
  176. ```tsx
  177. import { useMemo, useRef } from 'react';
  178. import {
  179. Renderer,
  180. StateProvider,
  181. VisibilityProvider,
  182. ActionProvider,
  183. } from '@json-render/react';
  184. import { registry, handlers } from './registry';
  185. function App({ spec, state, setState }) {
  186. const stateRef = useRef(state);
  187. const setStateRef = useRef(setState);
  188. stateRef.current = state;
  189. setStateRef.current = setState;
  190. const actionHandlers = useMemo(
  191. () => handlers(() => setStateRef.current, () => stateRef.current),
  192. [],
  193. );
  194. return (
  195. <StateProvider initialState={state}>
  196. <VisibilityProvider>
  197. <ActionProvider handlers={actionHandlers}>
  198. <Renderer spec={spec} registry={registry} />
  199. </ActionProvider>
  200. </VisibilityProvider>
  201. </StateProvider>
  202. );
  203. }
  204. ```
  205. ## @json-render/react-native
  206. `@json-render/react-native` uses the same `defineRegistry` API. The only difference is that components return React Native elements instead of HTML:
  207. ```tsx
  208. import { defineRegistry } from '@json-render/react-native';
  209. import { View, Text, Pressable } from 'react-native';
  210. export const { registry } = defineRegistry(catalog, {
  211. components: {
  212. Card: ({ props, children }) => (
  213. <View style={styles.card}>
  214. <Text style={styles.title}>{props.title}</Text>
  215. {children}
  216. </View>
  217. ),
  218. Button: ({ props, emit }) => (
  219. <Pressable onPress={() => emit("press")}>
  220. <Text>{props.label}</Text>
  221. </Pressable>
  222. ),
  223. },
  224. });
  225. ```
  226. See the [@json-render/react-native API reference](/docs/api/react-native) for the full API.
  227. ## @json-render/react-email
  228. `@json-render/react-email` uses `defineRegistry` like React and React Native. Components render to React Email primitives (`@react-email/components`). Use `renderToHtml` or `renderToPlainText` for server-side email output:
  229. ```tsx
  230. import { defineRegistry } from '@json-render/react-email';
  231. import { renderToHtml } from '@json-render/react-email';
  232. import { Body, Container, Heading, Text } from '@react-email/components';
  233. export const { registry } = defineRegistry(catalog, {
  234. components: {
  235. Card: ({ props, children }) => (
  236. <Container style={{ padding: 16, backgroundColor: '#fff' }}>
  237. <Heading>{props.title}</Heading>
  238. {children}
  239. </Container>
  240. ),
  241. },
  242. });
  243. const html = await renderToHtml(spec, { registry });
  244. ```
  245. See the [@json-render/react-email API reference](/docs/api/react-email) for the full API.
  246. ## @json-render/remotion
  247. `@json-render/remotion` takes a different approach. Instead of `defineRegistry`, it uses a plain component registry with built-in standard components for video production:
  248. ```tsx
  249. import { Renderer, standardComponents } from '@json-render/remotion';
  250. // Use the standard components directly
  251. <Renderer spec={timelineSpec} components={standardComponents} />
  252. // Or extend with your own
  253. const components = {
  254. ...standardComponents,
  255. CustomSlide: ({ clip }) => <AbsoluteFill>{/* ... */}</AbsoluteFill>,
  256. };
  257. ```
  258. The Remotion schema also supports `transitions` and `effects` in the catalog rather than actions.
  259. See the [@json-render/remotion API reference](/docs/api/remotion) for the full API.
  260. ## Next
  261. Learn about [data binding](/docs/data-binding) for dynamic values.