# 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 ```typescript 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 ```typescript 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 (
setQuery(e.target.value)} /> {filtered.map(item => (
{item.name}
))} {/* dialog content */}
) } ``` ### Build Atomic Components ```typescript // Atom: Single responsibility, no business logic export function ActionButton({ icon, label, onClick }: Props) { return ( ) } // Molecule: Composition of atoms export function SearchBar({ value, onChange, onClear }: Props) { return (
} onClick={onClear} variant="ghost" />
) } ``` ## Data Source Types ### KV (Persistent) ```typescript { id: 'todos', type: 'kv', key: 'app-todos', defaultValue: [] } ``` Data persists between sessions using the KV store. ### Static (Session Only) ```typescript { id: 'searchQuery', type: 'static', defaultValue: '' } ``` Data lives only in memory, reset on page reload. ### Computed (Derived) ```typescript { 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 ```typescript { type: 'create', target: 'todos', compute: (data) => ({ id: Date.now(), text: data.newTodo }) } ``` **Update**: Modify existing data ```typescript { type: 'update', target: 'todos', compute: (data) => data.todos.map(t => t.id === selectedId ? { ...t, completed: true } : t ) } ``` **Delete**: Remove items ```typescript { type: 'delete', target: 'todos', path: 'id', value: todoId } ``` ### UI Actions **Show Toast** ```typescript { type: 'show-toast', message: 'Task completed!', variant: 'success' // success | error | info | warning } ``` **Navigate** ```typescript { type: 'navigate', path: '/dashboard' } ``` ### Value Actions **Set Value** ```typescript { type: 'set-value', target: 'searchQuery', compute: (data, event) => event.target.value } ``` **Toggle Value** ```typescript { type: 'toggle-value', target: 'showCompleted' } ``` **Increment/Decrement** ```typescript { type: 'increment', target: 'counter', value: 1 } ``` ## Component Bindings Bind component props to data sources: ```typescript { id: 'input', type: 'Input', bindings: { value: { source: 'searchQuery' }, placeholder: { source: 'settings', path: 'inputPlaceholder' } } } ``` With transformations: ```typescript { bindings: { children: { source: 'count', transform: (value) => `${value} items` } } } ``` ## Event Handling ### Simple Event ```typescript { events: [ { event: 'click', actions: [{ type: 'show-toast', message: 'Clicked!' }] } ] } ``` ### Conditional Event ```typescript { events: [ { event: 'click', condition: (data) => data.searchQuery.length > 0, actions: [/* ... */] } ] } ``` ### Multiple Actions ```typescript { 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` Complete CRUD operations with KV persistence ```typescript const { items, create, read, update, remove, clear } = useCRUD({ key: 'todos', defaultValue: [], persist: true }) ``` #### `useSearch` Multi-field search with filtering ```typescript const { query, setQuery, filtered, resultCount } = useSearch({ items: todos, searchFields: ['text', 'tags'], caseSensitive: false }) ``` #### `useSort` Multi-key sorting with direction toggle ```typescript const { sorted, field, direction, toggleSort } = useSort({ items: todos, initialField: 'createdAt', initialDirection: 'desc' }) ``` #### `useJSONData` Flexible data management with optional persistence ```typescript const { value, setValue, updatePath, reset } = useJSONData({ key: 'user-prefs', defaultValue: {}, persist: true }) ``` ### UI Hooks #### `useDialog` Dialog/modal state management ```typescript const dialog = useDialog(false) // dialog.isOpen, dialog.open(), dialog.close(), dialog.toggle() ``` #### `useActionExecutor` Execute JSON-defined actions ```typescript 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 ```typescript // ❌ 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 ```typescript // ❌ 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 ```typescript // 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 ```typescript // Build complex UIs from simple atoms Dashboard (
} onClick={() => edit(item)} /> } onClick={() => remove(item.id)} />
)} />
``` ## Example: Complete Todo App See `/src/schemas/todo-list.json` 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` - **JSON Schemas**: `/src/schemas/*.json` - **Compute Functions**: `/src/schemas/compute-functions.ts` - **Schema Loader**: `/src/schemas/schema-loader.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**