| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304 |
- import { pageMetadata } from "@/lib/page-metadata"
- export const metadata = pageMetadata("docs/registry")
- # Registry
- A registry maps your [catalog](/docs/catalog) definitions to platform-specific implementations. The catalog defines *what* AI can generate — the registry provides the *how*.
- 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.
- - **`@json-render/react`** — Components (React elements) and action handlers
- - **`@json-render/react-native`** — Components (React Native elements) and action handlers
- - **`@json-render/remotion`** — Clip components, transitions, and effects
- ## @json-render/react
- ### defineRegistry
- Use `defineRegistry` to create a type-safe registry from your catalog. Pass your components, actions, or both:
- ```tsx
- import { defineRegistry } from '@json-render/react';
- import { myCatalog } from './catalog';
- export const { registry, handlers, executeAction } = defineRegistry(myCatalog, {
- components: {
- Card: ({ props, children }) => (
- <div className="card">
- <h2>{props.title}</h2>
- {props.description && <p>{props.description}</p>}
- {children}
- </div>
- ),
- Button: ({ props, emit }) => (
- <button onClick={() => emit("press")}>
- {props.label}
- </button>
- ),
- },
- actions: {
- submit_form: async (params, setState) => {
- const res = await fetch('/api/submit', {
- method: 'POST',
- body: JSON.stringify(params),
- });
- const result = await res.json();
- setState((prev) => ({ ...prev, formResult: result }));
- },
- export_data: async (params) => {
- const blob = await generateExport(params.format);
- downloadBlob(blob, `export.${params.format}`);
- },
- },
- });
- ```
- The returned object contains:
- - `registry` — component registry for `<Renderer />`
- - `handlers` — factory for ActionProvider-compatible handlers
- - `executeAction` — imperative action dispatch (for use outside the React tree)
- ### Component Props
- Each component receives a `ComponentContext` object:
- ```typescript
- interface ComponentContext {
- props: T; // Type-safe props from your catalog
- children?: React.ReactNode; // Rendered children (for slot components)
- emit: (event: string) => void; // Emit a named event (always defined)
- on: (event: string) => EventHandle; // Get event handle with metadata
- loading?: boolean; // Whether the renderer is in a loading state
- bindings?: Record<string, string>; // State paths from $bindState/$bindItem expressions
- }
- interface EventHandle {
- emit: () => void; // Fire the event
- shouldPreventDefault: boolean; // Whether any binding requested preventDefault
- bound: boolean; // Whether any handler is bound
- }
- ```
- Props are automatically inferred from your catalog, so `props.title` is typed as `string` if your catalog defines it that way.
- Use `emit("press")` for simple event firing. Use `on("click")` when you need to inspect event metadata:
- ```tsx
- Link: ({ props, on }) => {
- const click = on("click");
- return (
- <a
- href={props.href}
- onClick={(e) => {
- if (click.shouldPreventDefault) e.preventDefault();
- click.emit();
- }}
- >
- {props.label}
- </a>
- );
- },
- ```
- #### Using `bindings` for two-way binding
- 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:
- ```tsx
- import { useBoundProp, defineRegistry } from '@json-render/react';
- // Inside your registry:
- TextInput: ({ props, bindings }) => {
- const [value, setValue] = useBoundProp<string>(props.value, bindings?.value);
- return (
- <input
- value={value ?? ""}
- onChange={(e) => setValue(e.target.value)}
- />
- );
- },
- ```
- `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.
- ### Action Handlers
- Instead of AI generating arbitrary code, it declares *intent* by name. Your application provides the implementation. This is a core guardrail.
- 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:
- ```typescript
- import { defineCatalog } from '@json-render/core';
- import { schema } from '@json-render/react/schema';
- import { z } from 'zod';
- const catalog = defineCatalog(schema, {
- components: { /* ... */ },
- actions: {
- submit_form: {
- params: z.object({
- formId: z.string(),
- }),
- description: 'Submit a form',
- },
- export_data: {
- params: z.object({
- format: z.enum(['csv', 'pdf', 'json']),
- }),
- },
- navigate: {
- params: z.object({
- url: z.string(),
- }),
- },
- },
- });
- ```
- Action handlers receive `(params, setState, state)` and are defined inside `defineRegistry`:
- ```tsx
- export const { handlers, executeAction } = defineRegistry(catalog, {
- actions: {
- submit_form: async (params, setState) => {
- const response = await fetch('/api/submit', {
- method: 'POST',
- body: JSON.stringify({ formId: params.formId }),
- });
- const result = await response.json();
- setState((prev) => ({ ...prev, formResult: result }));
- },
- export_data: async (params) => {
- const blob = await generateExport(params.format);
- downloadBlob(blob, `export.${params.format}`);
- },
- navigate: (params) => {
- window.location.href = params.url;
- },
- },
- });
- ```
- ### Data Binding
- 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.
- 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]`:
- ```tsx
- import { useBoundProp } from '@json-render/react';
- // Inside defineRegistry components:
- Input: ({ props, bindings }) => {
- const [value, setValue] = useBoundProp<string>(
- props.value,
- bindings?.value
- );
- return (
- <input
- value={value ?? ''}
- onChange={(e) => setValue(e.target.value)}
- placeholder={props.placeholder}
- />
- );
- },
- ```
- 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`.
- ### Using the Renderer
- Wire everything together with providers and the `<Renderer />` component:
- ```tsx
- import { useMemo, useRef } from 'react';
- import {
- Renderer,
- StateProvider,
- VisibilityProvider,
- ActionProvider,
- } from '@json-render/react';
- import { registry, handlers } from './registry';
- function App({ spec, state, setState }) {
- const stateRef = useRef(state);
- const setStateRef = useRef(setState);
- stateRef.current = state;
- setStateRef.current = setState;
- const actionHandlers = useMemo(
- () => handlers(() => setStateRef.current, () => stateRef.current),
- [],
- );
- return (
- <StateProvider initialState={state}>
- <VisibilityProvider>
- <ActionProvider handlers={actionHandlers}>
- <Renderer spec={spec} registry={registry} />
- </ActionProvider>
- </VisibilityProvider>
- </StateProvider>
- );
- }
- ```
- ## @json-render/react-native
- `@json-render/react-native` uses the same `defineRegistry` API. The only difference is that components return React Native elements instead of HTML:
- ```tsx
- import { defineRegistry } from '@json-render/react-native';
- import { View, Text, Pressable } from 'react-native';
- export const { registry } = defineRegistry(catalog, {
- components: {
- Card: ({ props, children }) => (
- <View style={styles.card}>
- <Text style={styles.title}>{props.title}</Text>
- {children}
- </View>
- ),
- Button: ({ props, emit }) => (
- <Pressable onPress={() => emit("press")}>
- <Text>{props.label}</Text>
- </Pressable>
- ),
- },
- });
- ```
- See the [@json-render/react-native API reference](/docs/api/react-native) for the full API.
- ## @json-render/remotion
- `@json-render/remotion` takes a different approach. Instead of `defineRegistry`, it uses a plain component registry with built-in standard components for video production:
- ```tsx
- import { Renderer, standardComponents } from '@json-render/remotion';
- // Use the standard components directly
- <Renderer spec={timelineSpec} components={standardComponents} />
- // Or extend with your own
- const components = {
- ...standardComponents,
- CustomSlide: ({ clip }) => <AbsoluteFill>{/* ... */}</AbsoluteFill>,
- };
- ```
- The Remotion schema also supports `transitions` and `effects` in the catalog rather than actions.
- See the [@json-render/remotion API reference](/docs/api/remotion) for the full API.
- ## Next
- Learn about [data binding](/docs/data-binding) for dynamic values.
|