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.

BeforeAfter
DataProviderStateProvider
data propinitialState prop
getValue / setValue propsRemoved (use useStateStore() hook for get / set)
useDatauseStateStore
useDataValueuseStateValue
useDataBindinguseStateBinding (deprecated, use useBoundProp instead)
DataModel typeStateModel 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 }
  }
}
BeforeAfter
{ "$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" }
BeforeAfter
{ 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:

RemovedReplacement
createCatalogdefineCatalog(schema, config)
generateCatalogPromptcatalog.prompt()
generateSystemPromptcatalog.prompt()
ComponentDefinitionUse catalog component config directly
CatalogConfigUse defineCatalog parameters
SystemPromptOptionsUse PromptOptions
LogicExpressionUse VisibilityCondition
AuthStateModel auth as regular state (e.g. /auth/isSignedIn)
evaluateLogicExpressionUse evaluateVisibility
createRendererFromCatalogUse defineRegistry
traverseTree (codegen)Use traverseSpec