Skip to content

Commit bb501e4

Browse files
committed
defineRegistry
1 parent 4ae049f commit bb501e4

File tree

9 files changed

+191
-140
lines changed

9 files changed

+191
-140
lines changed

README.md

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,12 @@ const catalog = defineCatalog(schema, {
5656
});
5757
```
5858

59-
### 2. Register Component Implementations
59+
### 2. Define Your Components
6060

6161
```tsx
62-
import { defineComponents } from "@json-render/react";
62+
import { defineRegistry, Renderer } from "@json-render/react";
6363

64-
const components = defineComponents(catalog, {
64+
const registry = defineRegistry(catalog, {
6565
Card: ({ props, children }) => (
6666
<div className="card">
6767
<h3>{props.title}</h3>
@@ -75,7 +75,7 @@ const components = defineComponents(catalog, {
7575
</div>
7676
),
7777
Button: ({ props, onAction }) => (
78-
<button onClick={() => onAction?.(props.action)}>
78+
<button onClick={() => onAction?.({ name: props.action })}>
7979
{props.label}
8080
</button>
8181
),
@@ -85,16 +85,8 @@ const components = defineComponents(catalog, {
8585
### 3. Render AI-Generated Specs
8686

8787
```tsx
88-
import { Renderer } from "@json-render/react";
89-
9088
function Dashboard({ spec }) {
91-
return (
92-
<Renderer
93-
spec={spec}
94-
catalog={catalog}
95-
components={components}
96-
/>
97-
);
89+
return <Renderer spec={spec} registry={registry} />;
9890
}
9991
```
10092

@@ -115,7 +107,7 @@ function Dashboard({ spec }) {
115107
### React (UI)
116108

117109
```tsx
118-
import { Renderer } from "@json-render/react";
110+
import { defineRegistry, Renderer } from "@json-render/react";
119111
import { schema } from "@json-render/react";
120112

121113
// Element tree spec format
@@ -129,7 +121,9 @@ const spec = {
129121
}
130122
};
131123

132-
<Renderer spec={spec} catalog={catalog} components={components} />
124+
// defineRegistry creates a type-safe component registry
125+
const registry = defineRegistry(catalog, components);
126+
<Renderer spec={spec} registry={registry} />
133127
```
134128

135129
### Remotion (Video)

apps/web/app/(main)/docs/api/react/page.tsx

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -43,36 +43,49 @@ interface AuthState {
4343
4444
type ValidatorFn = (value: unknown, args?: object) => boolean | Promise<boolean>;`}</Code>
4545

46-
<h2 className="text-xl font-semibold mt-12 mb-4">createRenderer</h2>
46+
<h2 className="text-xl font-semibold mt-12 mb-4">defineRegistry</h2>
4747
<p className="text-sm text-muted-foreground mb-4">
48-
Factory function to create a pre-configured renderer component.
48+
Create a type-safe component registry from a catalog. Components receive{" "}
49+
<code className="text-foreground">props</code>,{" "}
50+
<code className="text-foreground">children</code>,{" "}
51+
<code className="text-foreground">onAction</code>, and{" "}
52+
<code className="text-foreground">loading</code> with catalog-inferred
53+
types.
4954
</p>
50-
<Code lang="tsx">{`import { createRenderer } from '@json-render/react';
51-
52-
const MyRenderer = createRenderer({
53-
registry: componentRegistry,
54-
data?: initialData,
55-
actionHandlers?: actionHandlerMap,
56-
auth?: authState,
55+
<Code lang="tsx">{`import { defineRegistry } from '@json-render/react';
56+
57+
const registry = defineRegistry(catalog, {
58+
Card: ({ props, children }) => <div>{props.title}{children}</div>,
59+
Button: ({ props, onAction }) => (
60+
<button onClick={() => onAction?.({ name: props.action })}>
61+
{props.label}
62+
</button>
63+
),
5764
});
5865
59-
// Usage
60-
<MyRenderer spec={spec} loading={isStreaming} />`}</Code>
66+
// Pass to <Renderer>
67+
<Renderer spec={spec} registry={registry} />`}</Code>
6168

6269
<h2 className="text-xl font-semibold mt-12 mb-4">Components</h2>
6370

6471
<h3 className="text-lg font-semibold mt-8 mb-4">Renderer</h3>
6572
<Code lang="tsx">{`<Renderer
6673
spec={Spec} // The UI spec to render
67-
registry={Registry} // Component registry
74+
registry={Registry} // Component registry (from defineRegistry)
6875
loading={boolean} // Optional loading state
76+
fallback={Component} // Optional fallback for unknown types
6977
/>
7078
71-
type Registry = Record<string, React.ComponentType<ComponentProps>>;
79+
type Registry = Record<string, React.ComponentType<ComponentRenderProps>>;`}</Code>
7280

73-
interface ComponentProps<T = Record<string, unknown>> {
74-
props: T; // Component props from spec
81+
<h3 className="text-lg font-semibold mt-8 mb-4">
82+
Component Props (via defineRegistry)
83+
</h3>
84+
<Code lang="tsx">{`interface ComponentContext<P> {
85+
props: P; // Typed props from catalog
7586
children?: React.ReactNode; // Rendered children (for slot components)
87+
onAction?: (action: { name: string; params?: object }) => void;
88+
loading?: boolean;
7689
}`}</Code>
7790

7891
<h2 className="text-xl font-semibold mt-12 mb-4">Hooks</h2>

apps/web/app/(main)/docs/quick-start/page.tsx

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -61,19 +61,20 @@ export const catalog = defineCatalog(schema, {
6161
});`}</Code>
6262

6363
<h2 className="text-xl font-semibold mt-12 mb-4">
64-
2. Create your components and actions
64+
2. Define your components
6565
</h2>
6666
<p className="text-sm text-muted-foreground mb-4">
67-
Define React components that render each catalog type. Each component
68-
receives <code className="text-foreground">props</code>,{" "}
67+
Use <code className="text-foreground">defineRegistry</code> to map
68+
catalog types to React components. Each component receives type-safe{" "}
69+
<code className="text-foreground">props</code>,{" "}
6970
<code className="text-foreground">children</code>, and{" "}
7071
<code className="text-foreground">onAction</code>:
7172
</p>
72-
<Code lang="tsx">{`// lib/components.tsx
73-
import { defineComponents } from '@json-render/react';
73+
<Code lang="tsx">{`// lib/registry.tsx
74+
import { defineRegistry } from '@json-render/react';
7475
import { catalog } from './catalog';
7576
76-
export const components = defineComponents(catalog, {
77+
export const registry = defineRegistry(catalog, {
7778
Card: ({ props, children }) => (
7879
<div className="p-4 border rounded-lg">
7980
<h2 className="font-bold">{props.title}</h2>
@@ -86,7 +87,7 @@ export const components = defineComponents(catalog, {
8687
Button: ({ props, onAction }) => (
8788
<button
8889
className="px-4 py-2 bg-blue-500 text-white rounded"
89-
onClick={() => onAction?.(props.action)}
90+
onClick={() => onAction?.({ name: props.action })}
9091
>
9192
{props.label}
9293
</button>
@@ -123,14 +124,14 @@ export async function POST(req: Request) {
123124

124125
<h2 className="text-xl font-semibold mt-12 mb-4">4. Render the UI</h2>
125126
<p className="text-sm text-muted-foreground mb-4">
126-
Use the providers and renderer to display AI-generated UI:
127+
Use providers and the <code className="text-foreground">Renderer</code>{" "}
128+
with your registry to display AI-generated UI:
127129
</p>
128130
<Code lang="tsx">{`// app/page.tsx
129131
'use client';
130132
131-
import { DataProvider, ActionProvider, VisibilityProvider, Renderer, useUIStream } from '@json-render/react';
132-
import { catalog } from '@/lib/catalog';
133-
import { components } from '@/lib/components';
133+
import { Renderer, DataProvider, ActionProvider, VisibilityProvider, useUIStream } from '@json-render/react';
134+
import { registry } from '@/lib/registry';
134135
135136
export default function Page() {
136137
const { spec, isStreaming, send } = useUIStream({
@@ -162,7 +163,7 @@ export default function Page() {
162163
</form>
163164
164165
<div className="mt-8">
165-
<Renderer spec={spec} catalog={catalog} components={components} loading={isStreaming} />
166+
<Renderer spec={spec} registry={registry} loading={isStreaming} />
166167
</div>
167168
</ActionProvider>
168169
</VisibilityProvider>

examples/dashboard/lib/render/renderer.tsx

Lines changed: 33 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,24 @@
22

33
import { useMemo, useRef, type ReactNode } from "react";
44
import {
5+
defineRegistry,
56
Renderer,
6-
type ComponentRegistry,
7+
type ComponentRenderer,
78
type Spec,
89
DataProvider,
910
VisibilityProvider,
1011
ActionProvider,
1112
} from "@json-render/react";
1213

14+
import { dashboardCatalog } from "./catalog";
1315
import { components, Fallback } from "./catalog/components";
14-
import { executeAction } from "./catalog/actions";
16+
import { actionHandlers, executeAction } from "./catalog/actions";
17+
18+
// =============================================================================
19+
// Registry - created once from the catalog and component map
20+
// =============================================================================
21+
22+
const registry = defineRegistry(dashboardCatalog, components);
1523

1624
// =============================================================================
1725
// DashboardRenderer
@@ -29,39 +37,9 @@ interface DashboardRendererProps {
2937
loading?: boolean;
3038
}
3139

32-
// Build registry - uses refs to avoid recreating on data changes
33-
function buildRegistry(
34-
dataRef: React.RefObject<Record<string, unknown>>,
35-
setDataRef: React.RefObject<SetData | undefined>,
36-
loading?: boolean,
37-
): ComponentRegistry {
38-
const registry: ComponentRegistry = {};
39-
40-
for (const [name, componentFn] of Object.entries(components)) {
41-
registry[name] = (renderProps: {
42-
element: { props: Record<string, unknown> };
43-
children?: ReactNode;
44-
}) =>
45-
componentFn({
46-
props: renderProps.element.props as never,
47-
children: renderProps.children,
48-
onAction: (a) => {
49-
const setData = setDataRef.current;
50-
const data = dataRef.current;
51-
if (setData) {
52-
executeAction(a.name, a.params, setData, data);
53-
}
54-
},
55-
loading,
56-
});
57-
}
58-
59-
return registry;
60-
}
61-
6240
// Fallback component for unknown types
63-
const fallbackRegistry = (renderProps: { element: { type: string } }) => (
64-
<Fallback type={renderProps.element.type} />
41+
const fallback: ComponentRenderer = ({ element }) => (
42+
<Fallback type={element.type} />
6543
);
6644

6745
export function DashboardRenderer({
@@ -71,28 +49,41 @@ export function DashboardRenderer({
7149
onDataChange,
7250
loading,
7351
}: DashboardRendererProps): ReactNode {
74-
// Use refs to keep registry stable while still accessing latest data/setData
52+
// Use refs so action handlers always see the latest data/setData
7553
const dataRef = useRef(data);
7654
const setDataRef = useRef(setData);
7755
dataRef.current = data;
7856
setDataRef.current = setData;
7957

80-
// Memoize registry - only changes when loading changes
81-
const registry = useMemo(
82-
() => buildRegistry(dataRef, setDataRef, loading),
83-
[loading],
84-
);
58+
// Create ActionProvider-compatible handlers that delegate to the
59+
// dashboard's executeAction (which needs setData + data from refs)
60+
const handlers = useMemo(() => {
61+
const result: Record<
62+
string,
63+
(params: Record<string, unknown>) => Promise<void>
64+
> = {};
65+
for (const name of Object.keys(actionHandlers)) {
66+
result[name] = async (params) => {
67+
const sd = setDataRef.current;
68+
const d = dataRef.current;
69+
if (sd) {
70+
await executeAction(name, params, sd, d);
71+
}
72+
};
73+
}
74+
return result;
75+
}, []);
8576

8677
if (!spec) return null;
8778

8879
return (
8980
<DataProvider initialData={data} onDataChange={onDataChange}>
9081
<VisibilityProvider>
91-
<ActionProvider>
82+
<ActionProvider handlers={handlers}>
9283
<Renderer
9384
spec={spec}
9485
registry={registry}
95-
fallback={fallbackRegistry}
86+
fallback={fallback}
9687
loading={loading}
9788
/>
9889
</ActionProvider>

examples/remotion/tsconfig.tsbuildinfo

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)