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();
// Inline mode prompt (formerly "chat")
const inlinePrompt = catalog.prompt({ mode: "inline" });Generation Modes#
The generation mode values passed to catalog.prompt() have been renamed for clarity:
"generate"is now"standalone""chat"is now"inline"
The old names are accepted as deprecated aliases, so existing code will continue to work. Update when convenient.
Before:
const prompt = catalog.prompt({ mode: "generate" });
const chatPrompt = catalog.prompt({ mode: "chat" });After:
const prompt = catalog.prompt({ mode: "standalone" });
const inlinePrompt = catalog.prompt({ mode: "inline" });The default mode (when no mode option is provided) is "standalone", which behaves identically to the previous "generate" default.
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 |