# 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}
))}
)
}
```
### 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 (
)
}
```
## 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`
}
}
}
```
## Component Pattern Templates
Use these patterns as starting points when authoring JSON schemas. Each example includes
recommended prop shapes and binding strategies for predictable rendering and data flow.
### Form Pattern (Create/Edit)
**Recommended prop shape**
- `name`: field identifier used in data mappings.
- `label`: user-facing label.
- `placeholder`: optional hint text.
- `type`: input type (`text`, `email`, `number`, `date`, etc.).
- `required`: boolean for validation UI.
**Schema example**
```typescript
{
id: 'profile-form',
type: 'form',
props: {
className: 'space-y-4'
},
children: [
{
id: 'first-name',
type: 'Input',
props: {
name: 'firstName',
label: 'First name',
placeholder: 'Ada',
required: true
},
bindings: {
value: { source: 'formState', path: 'firstName' }
},
events: [
{
event: 'change',
actions: [
{
type: 'set-value',
target: 'formState',
path: 'firstName',
compute: (data, event) => event.target.value
}
]
}
]
},
{
id: 'email',
type: 'Input',
props: {
name: 'email',
label: 'Email',
placeholder: 'ada@lovelace.dev',
type: 'email'
},
bindings: {
value: { source: 'formState', path: 'email' }
},
events: [
{
event: 'change',
actions: [
{
type: 'set-value',
target: 'formState',
path: 'email',
compute: (data, event) => event.target.value
}
]
}
]
},
{
id: 'save-profile',
type: 'Button',
props: { children: 'Save profile' },
events: [
{
event: 'click',
actions: [
{
type: 'create',
target: 'profiles',
compute: (data) => ({
id: Date.now(),
...data.formState
})
},
{
type: 'set-value',
target: 'formState',
value: { firstName: '', email: '' }
}
]
}
]
}
]
}
```
**Recommended bindings**
- Use `bindings.value` for inputs and update a single `formState` data source.
- Use `set-value` with `path` to update individual fields and avoid cloning the whole object.
### Card Pattern (Summary/Stat)
**Recommended prop shape**
- `title`: primary label.
- `description`: supporting copy.
- `badge`: optional status tag.
- `icon`: optional leading icon name or component id.
**Schema example**
```typescript
{
id: 'stats-card',
type: 'Card',
props: { className: 'p-4' },
children: [
{
id: 'card-header',
type: 'div',
props: { className: 'flex items-center justify-between' },
children: [
{
id: 'card-title',
type: 'h3',
bindings: {
children: { source: 'stats', path: 'title' }
},
props: { className: 'text-lg font-semibold' }
},
{
id: 'card-badge',
type: 'Badge',
bindings: {
children: { source: 'stats', path: 'status' },
variant: {
source: 'stats',
path: 'status',
transform: (value) => (value === 'Active' ? 'success' : 'secondary')
}
}
}
]
},
{
id: 'card-description',
type: 'p',
props: { className: 'text-sm text-muted-foreground' },
bindings: {
children: { source: 'stats', path: 'description' }
}
}
]
}
```
**Recommended bindings**
- Bind the card text fields directly to a `stats` data source.
- Use `transform` for simple presentation mappings (status to badge variant).
### List Pattern (Collection + Row Actions)
**Recommended prop shape**
- `items`: array data source bound at the list container.
- `keyField`: unique field for list keys.
- `primary`: main text content (usually `name` or `title`).
- `secondary`: supporting text (optional).
- `actions`: array of action configs for row-level events.
**Schema example**
```typescript
{
id: 'task-list',
type: 'div',
bindings: {
children: {
source: 'tasks',
transform: (items) =>
items.map((item) => ({
id: `task-${item.id}`,
type: 'div',
props: { className: 'flex items-center justify-between py-2' },
children: [
{
id: `task-name-${item.id}`,
type: 'span',
bindings: {
children: { source: 'item', path: 'name' }
}
},
{
id: `task-toggle-${item.id}`,
type: 'Button',
props: { size: 'sm', variant: 'outline' },
bindings: {
children: {
source: 'item',
path: 'completed',
transform: (value) => (value ? 'Undo' : 'Complete')
}
},
events: [
{
event: 'click',
actions: [
{
type: 'update',
target: 'tasks',
id: item.id,
compute: (data) => ({
...item,
completed: !item.completed
})
}
]
}
]
}
]
}))
}
}
}
```
**Recommended bindings**
- Use a `transform` to map collection items into child component schemas.
- Use `{ source: 'item', path: 'field' }` when binding inside the item loop for clarity and efficiency.
## 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
```
## 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**