12k

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