Migration Guide
This guide covers breaking changes introduced in v0.6.0 and how to update your code.
State Provider
DataProvider has been renamed to StateProvider, and its props have changed.
Before:
import { DataProvider } from "@json-render/react";
<DataProvider data={myData} getValue={getter} setValue={setter}>
{children}
</DataProvider>After:
import { StateProvider } from "@json-render/react";
<StateProvider initialState={myData} onStateChange={(path, value) => console.log(path, value)}>
{children}
</StateProvider>StateProvider now manages state internally. Use useStateStore() to access get, set, and update.
| Before | After |
|--------|-------|
| DataProvider | StateProvider |
| data prop | initialState prop |
| getValue / setValue props | Removed (use useStateStore() hook for get / set) |
| useData | useStateStore |
| useDataValue | useStateValue |
| useDataBinding | useStateBinding (deprecated, use useBoundProp instead) |
| DataModel type | StateModel type |
Dynamic Expressions
All dynamic value expressions have been renamed to use $state, $item, and $index.
Before:
{
"type": "Text",
"props": {
"label": { "$path": "/user/name" },
"count": { "$data": "/items/length" }
}
}After:
{
"type": "Text",
"props": {
"label": { "$state": "/user/name" },
"count": { "$state": "/items/length" }
}
}Inside repeat scopes, use $item and $index:
{
"type": "Card",
"props": {
"title": { "$item": "name" },
"subtitle": { "$index": true }
}
}| Before | After |
|--------|-------|
| { "$path": "/..." } | { "$state": "/..." } |
| { "$data": "/..." } | { "$state": "/..." } |
Two-Way Binding
Form components no longer use valuePath / statePath props. Instead, use $bindState expressions on the value prop, and useBoundProp in your registry.
Before (catalog):
Input: {
props: z.object({
label: z.string(),
valuePath: z.string(),
placeholder: z.string().optional(),
}),
}Before (spec):
{
"type": "Input",
"props": { "label": "Email", "valuePath": "/form/email" }
}Before (registry):
Input: ({ props }) => {
const [value, setValue] = useStateBinding(props.valuePath);
return <input value={value ?? ""} onChange={(e) => setValue(e.target.value)} />;
}After (catalog):
Input: {
props: z.object({
label: z.string(),
value: z.string().optional(),
placeholder: z.string().optional(),
}),
}After (spec):
{
"type": "Input",
"props": { "label": "Email", "value": { "$bindState": "/form/email" } }
}After (registry):
Input: ({ props, bindings }) => {
const [value, setValue] = useBoundProp<string>(props.value, bindings?.value);
return <input value={value ?? ""} onChange={(e) => setValue(e.target.value)} />;
}$bindState reads from and writes to the given state path. Inside repeat scopes, use $bindItem to bind to a field on the current item:
{
"type": "Checkbox",
"props": { "checked": { "$bindItem": "completed" } }
}Visibility Conditions
Visibility conditions have been renamed to use $state, $and, and $or.
Before:
{ "path": "/isAdmin" }
{ "eq": [{ "path": "/role" }, "admin"] }
{ "and": [{ "path": "/isAdmin" }, { "path": "/feature" }] }
{ "or": [{ "path": "/roleA" }, { "path": "/roleB" }] }After:
{ "$state": "/isAdmin" }
{ "$state": "/role", "eq": "admin" }
{ "$and": [{ "$state": "/isAdmin" }, { "$state": "/feature" }] }
{ "$or": [{ "$state": "/roleA" }, { "$state": "/roleB" }] }You can also use an array as shorthand for $and:
[{ "$state": "/isAdmin" }, { "$state": "/feature" }]Inside repeat scopes, use $item and $index:
{ "$item": "isActive" }
{ "$index": true, "eq": 0 }Event System
Components now use emit to fire named events. onAction has been removed.
Before:
Button: ({ props, onAction }) => (
<button onClick={() => onAction?.("press")}>{props.label}</button>
)After:
Button: ({ props, emit }) => (
<button onClick={() => emit("press")}>{props.label}</button>
)emit is always defined (never undefined), so optional chaining is not needed.
Actions Context
dispatch has been renamed to execute, and the provider prop has been renamed from actionHandlers to handlers.
Before:
const { dispatch } = useActions();
dispatch({ action: "submit", params: {} });
<ActionProvider actionHandlers={myHandlers}>After:
const { execute } = useActions();
execute({ action: "submit", params: {} });
<ActionProvider handlers={myHandlers}>Repeat / List Rendering
The repeat field now uses statePath instead of path.
Before:
{
"type": "Column",
"repeat": { "path": "/todos", "key": "id" },
"children": ["todo-item"]
}After:
{
"type": "Column",
"repeat": { "statePath": "/todos", "key": "id" },
"children": ["todo-item"]
}Catalog Creation
createCatalog and generateSystemPrompt have been replaced by defineSchema + defineCatalog.
Before:
import { createCatalog, generateSystemPrompt } from "@json-render/core";
const catalog = createCatalog({
name: "my-app",
components: { /* ... */ },
actions: { /* ... */ },
});
const prompt = generateSystemPrompt(catalog);After:
import { defineCatalog } from "@json-render/core";
import { schema } from "@json-render/react/schema";
const catalog = defineCatalog(schema, {
components: { /* ... */ },
actions: { /* ... */ },
});
const prompt = catalog.prompt();
// Chat mode prompt
const chatPrompt = catalog.prompt({ mode: "chat" });Validation
ValidationCheck now uses type instead of fn, ValidationProvider uses customFunctions instead of functions, and useFieldValidation takes a config object instead of a checks array.
Before:
{ "fn": "required", "message": "Required" }
{ "fn": "minLength", "args": { "length": 8 }, "message": "Too short" }After:
{ "type": "required", "message": "Required" }
{ "type": "minLength", "args": { "min": 8 }, "message": "Too short" }| Before | After |
|--------|-------|
| { fn: "required" } | { type: "required" } |
| ValidationProvider functions={...} | ValidationProvider customFunctions={...} |
| useFieldValidation(path, checks) | useFieldValidation(path, config) where config is { checks, validateOn? } |
Visibility Provider
The auth prop has been removed from VisibilityProvider. Auth state should be modeled as regular state.
Before:
<VisibilityProvider auth={{ isSignedIn: true, role: "admin" }}>{ "auth": "signedIn" }After:
<StateProvider initialState={{ auth: { isSignedIn: true, role: "admin" } }}>
<VisibilityProvider>{ "$state": "/auth/isSignedIn" }Codegen
traverseTree has been renamed to traverseSpec, SpecVisitor to TreeVisitor, and the visitor callback now receives a key parameter.
Before:
import { traverseTree } from "@json-render/codegen";
traverseTree(tree, (element) => {
// ...
});After:
import { traverseSpec } from "@json-render/codegen";
traverseSpec(spec, (element, key) => {
// ...
});Action Params
Action params in specs now use statePath instead of path.
Before:
{
"on": {
"press": { "action": "setState", "params": { "path": "/count", "value": 0 } }
}
}After:
{
"on": {
"press": { "action": "setState", "params": { "statePath": "/count", "value": 0 } }
}
}Removed Exports
The following exports have been removed from @json-render/core:
| Removed | Replacement |
|---------|-------------|
| createCatalog | defineCatalog(schema, config) |
| generateCatalogPrompt | catalog.prompt() |
| generateSystemPrompt | catalog.prompt() |
| ComponentDefinition | Use catalog component config directly |
| CatalogConfig | Use defineCatalog parameters |
| SystemPromptOptions | Use PromptOptions |
| LogicExpression | Use VisibilityCondition |
| AuthState | Model auth as regular state (e.g. /auth/isSignedIn) |
| evaluateLogicExpression | Use evaluateVisibility |
| createRendererFromCatalog | Use defineRegistry |
| traverseTree (codegen) | Use traverseSpec |