Files
low-code-react-app-b/docs/JSON_UI_GUIDE.md
2026-01-17 20:41:48 +00:00

12 KiB

JSON-Driven UI Architecture Guide

Overview

This application uses a declarative JSON-driven architecture that allows you to build entire user interfaces from configuration rather than code. Combined with atomic components (all under 150 LOC) and custom hooks for business logic, this creates a highly maintainable and rapidly prototype-able system.

Core Concepts

1. JSON Schema Definition

Define your entire UI using JSON schemas with:

  • Data Sources: KV store, computed values, static data
  • Components: Shadcn UI components with props and bindings
  • Actions: CRUD operations, toasts, navigation
  • Events: User interactions that trigger actions

2. Atomic Components

Small, focused, reusable components:

  • Atoms (< 50 LOC): ActionButton, IconButton, DataList, LoadingSpinner
  • Molecules (50-100 LOC): StatCard, EmptyState, SearchBar
  • Organisms (100-150 LOC): DataTable, FormBuilder, Dashboard

3. Custom Hooks

Extract business logic into reusable hooks:

  • Data Management: useCRUD, useSearch, useSort, useJSONData
  • UI State: useDialog, useActionExecutor
  • Forms: useForm, useFormField

Quick Start

Define a Page Schema

import { PageSchema } from '@/types/json-ui'

export const myPageSchema: PageSchema = {
  id: 'my-page',
  name: 'My Page',
  layout: { type: 'single' },
  
  // Data sources
  dataSources: [
    {
      id: 'items',
      type: 'kv',              // Persisted to KV store
      key: 'my-items',
      defaultValue: []
    },
    {
      id: 'stats',
      type: 'computed',         // Computed from other data
      compute: (data) => ({
        total: data.items?.length || 0
      }),
      dependencies: ['items']
    }
  ],
  
  // UI components
  components: [
    {
      id: 'root',
      type: 'div',
      props: { className: 'p-6' },
      children: [
        {
          id: 'title',
          type: 'h1',
          props: { 
            className: 'text-3xl font-bold',
            children: 'My Page'
          }
        },
        {
          id: 'add-button',
          type: 'Button',
          props: { children: 'Add Item' },
          events: [
            {
              event: 'click',
              actions: [
                {
                  id: 'create-item',
                  type: 'create',
                  target: 'items',
                  compute: (data) => ({
                    id: Date.now(),
                    name: 'New Item'
                  })
                },
                {
                  id: 'show-toast',
                  type: 'show-toast',
                  message: 'Item added!',
                  variant: 'success'
                }
              ]
            }
          ]
        }
      ]
    }
  ]
}

Use Custom Hooks

import { useCRUD, useSearch, useDialog } from '@/hooks'

function MyComponent() {
  // CRUD operations with persistence
  const { items, create, update, remove } = useCRUD({
    key: 'my-items',
    defaultValue: [],
    persist: true
  })
  
  // Search functionality
  const { query, setQuery, filtered } = useSearch({
    items,
    searchFields: ['name', 'description']
  })
  
  // Dialog state management
  const dialog = useDialog()
  
  return (
    <div>
      <input value={query} onChange={(e) => setQuery(e.target.value)} />
      {filtered.map(item => (
        <div key={item.id}>{item.name}</div>
      ))}
      <button onClick={dialog.open}>Open</button>
      <Dialog open={dialog.isOpen} onOpenChange={dialog.setOpen}>
        {/* dialog content */}
      </Dialog>
    </div>
  )
}

Build Atomic Components

// Atom: Single responsibility, no business logic
export function ActionButton({ icon, label, onClick }: Props) {
  return (
    <Button onClick={onClick}>
      {icon}
      {label && <span className="ml-2">{label}</span>}
    </Button>
  )
}

// Molecule: Composition of atoms
export function SearchBar({ value, onChange, onClear }: Props) {
  return (
    <div className="flex gap-2">
      <Input value={value} onChange={onChange} />
      <ActionButton 
        icon={<X />} 
        onClick={onClear}
        variant="ghost"
      />
    </div>
  )
}

Data Source Types

KV (Persistent)

{
  id: 'todos',
  type: 'kv',
  key: 'app-todos',
  defaultValue: []
}

Data persists between sessions using the KV store.

Static (Session Only)

{
  id: 'searchQuery',
  type: 'static',
  defaultValue: ''
}

Data lives only in memory, reset on page reload.

Computed (Derived)

{
  id: 'stats',
  type: 'computed',
  compute: (data) => ({
    total: data.todos.length,
    completed: data.todos.filter(t => t.completed).length
  }),
  dependencies: ['todos']
}

Automatically recomputes when dependencies change.

Action Types

CRUD Actions

Create: Add new items

{
  type: 'create',
  target: 'todos',
  compute: (data) => ({ 
    id: Date.now(), 
    text: data.newTodo 
  })
}

Update: Modify existing data

{
  type: 'update',
  target: 'todos',
  compute: (data) => 
    data.todos.map(t => 
      t.id === selectedId ? { ...t, completed: true } : t
    )
}

Delete: Remove items

{
  type: 'delete',
  target: 'todos',
  path: 'id',
  value: todoId
}

UI Actions

Show Toast

{
  type: 'show-toast',
  message: 'Task completed!',
  variant: 'success' // success | error | info | warning
}

Navigate

{
  type: 'navigate',
  path: '/dashboard'
}

Value Actions

Set Value

{
  type: 'set-value',
  target: 'searchQuery',
  compute: (data, event) => event.target.value
}

Toggle Value

{
  type: 'toggle-value',
  target: 'showCompleted'
}

Increment/Decrement

{
  type: 'increment',
  target: 'counter',
  value: 1
}

Component Bindings

Bind component props to data sources:

{
  id: 'input',
  type: 'Input',
  bindings: {
    value: { 
      source: 'searchQuery' 
    },
    placeholder: {
      source: 'settings',
      path: 'inputPlaceholder'
    }
  }
}

With transformations:

{
  bindings: {
    children: {
      source: 'count',
      transform: (value) => `${value} items`
    }
  }
}

Event Handling

Simple Event

{
  events: [
    {
      event: 'click',
      actions: [{ type: 'show-toast', message: 'Clicked!' }]
    }
  ]
}

Conditional Event

{
  events: [
    {
      event: 'click',
      condition: (data) => data.searchQuery.length > 0,
      actions: [/* ... */]
    }
  ]
}

Multiple Actions

{
  events: [
    {
      event: 'click',
      actions: [
        { type: 'create', target: 'items', /* ... */ },
        { type: 'set-value', target: 'input', value: '' },
        { type: 'show-toast', message: 'Added!' }
      ]
    }
  ]
}

Available Custom Hooks

Data Hooks

useCRUD<T>

Complete CRUD operations with KV persistence

const { items, create, read, update, remove, clear } = useCRUD({
  key: 'todos',
  defaultValue: [],
  persist: true
})

useSearch<T>

Multi-field search with filtering

const { query, setQuery, filtered, resultCount } = useSearch({
  items: todos,
  searchFields: ['text', 'tags'],
  caseSensitive: false
})

useSort<T>

Multi-key sorting with direction toggle

const { sorted, field, direction, toggleSort } = useSort({
  items: todos,
  initialField: 'createdAt',
  initialDirection: 'desc'
})

useJSONData

Flexible data management with optional persistence

const { value, setValue, updatePath, reset } = useJSONData({
  key: 'user-prefs',
  defaultValue: {},
  persist: true
})

UI Hooks

useDialog

Dialog/modal state management

const dialog = useDialog(false)
// dialog.isOpen, dialog.open(), dialog.close(), dialog.toggle()

useActionExecutor

Execute JSON-defined actions

const { executeAction, executeActions } = useActionExecutor(context)
await executeAction({ type: 'create', target: 'items', /* ... */ })

Best Practices

1. Keep Components Small

  • Atoms: < 50 LOC
  • Molecules: 50-100 LOC
  • Organisms: 100-150 LOC
  • If larger, split into smaller pieces

2. Extract Logic to Hooks

// ❌ Bad: Logic in component
function TodoList() {
  const [todos, setTodos] = useState([])
  const addTodo = (text) => setTodos([...todos, { id: Date.now(), text }])
  const removeTodo = (id) => setTodos(todos.filter(t => t.id !== id))
  // ...
}

// ✅ Good: Logic in hook
function TodoList() {
  const { items: todos, create, remove } = useCRUD({
    key: 'todos',
    defaultValue: []
  })
  // Component only handles UI
}

3. Use Computed Data Sources

// ❌ Bad: Computing in render
components: [{
  type: 'div',
  props: {
    children: `${data.todos.filter(t => t.completed).length} completed`
  }
}]

// ✅ Good: Computed data source
dataSources: [
  {
    id: 'stats',
    type: 'computed',
    compute: (data) => ({
      completed: data.todos.filter(t => t.completed).length
    })
  }
],
components: [{
  bindings: {
    children: {
      source: 'stats',
      path: 'completed',
      transform: (v) => `${v} completed`
    }
  }
}]

4. Chain Actions

// Multiple related actions in sequence
events: [{
  event: 'click',
  actions: [
    { type: 'create', /* ... */ },      // 1. Add item
    { type: 'set-value', /* ... */ },   // 2. Clear input
    { type: 'show-toast', /* ... */ }   // 3. Show feedback
  ]
}]

5. Leverage Atomic Composition

// Build complex UIs from simple atoms
<Card>
  <CardHeader>
    <CardTitle>Dashboard</CardTitle>
  </CardHeader>
  <CardContent>
    <DataList
      items={filtered}
      renderItem={(item) => (
        <div className="flex gap-2">
          <ActionButton icon={<Edit />} onClick={() => edit(item)} />
          <ActionButton icon={<Trash />} onClick={() => remove(item.id)} />
        </div>
      )}
    />
  </CardContent>
</Card>

Example: Complete Todo App

See /src/schemas/page-schemas.ts for a full working example with:

  • KV persistence
  • Computed statistics
  • CRUD operations
  • Action chaining
  • Conditional rendering
  • Event handling

Migration Strategy

Phase 1: Extract Hooks

  1. Identify repeated logic patterns
  2. Create custom hooks
  3. Replace inline logic with hook calls

Phase 2: Break Down Components

  1. Identify large components (>150 LOC)
  2. Split into atoms, molecules, organisms
  3. Compose back together

Phase 3: Define JSON Schemas

  1. Convert simple pages to JSON first
  2. Test with PageRenderer
  3. Gradually migrate complex pages

Performance Tips

  1. Use useMemo for expensive computations
  2. Implement virtual scrolling for large lists
  3. Lazy load heavy components
  4. Debounce search and filter operations
  5. Use computed data sources instead of computing in render

Troubleshooting

Data not persisting?

  • Check data source type is 'kv'
  • Verify key is unique
  • Ensure useKV is called unconditionally

Actions not executing?

  • Check action type spelling
  • Verify target matches data source id
  • Ensure compute function returns correct type

Components not rendering?

  • Check component type is registered
  • Verify props match component API
  • Check conditional bindings

Resources

  • Type Definitions: /src/types/json-ui.ts
  • Page Schemas: /src/schemas/page-schemas.ts
  • Custom Hooks: /src/hooks/data/ and /src/hooks/ui/
  • Atomic Components: /src/components/atoms/
  • Component Registry: /src/lib/json-ui/component-registry.ts

Built with ❤️ using React, TypeScript, and Shadcn UI