- codegen: Low-code React app with JSON-driven component system - packagerepo: Schema-driven package repository with backend/frontend - postgres: Next.js app with Drizzle ORM and PostgreSQL Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
8.1 KiB
Data Source Binding Guide
Overview
The Data Source Binding system enables declarative data management in CodeForge applications. Instead of manually managing React state, you define data sources and bind them directly to component properties.
Data Source Types
1. KV Store (kv)
Persistent data storage backed by the Spark KV API. Perfect for user preferences, application state, and any data that needs to survive page refreshes.
{
id: 'userProfile',
type: 'kv',
key: 'user-profile-data',
defaultValue: {
name: 'John Doe',
email: 'john@example.com',
preferences: {
theme: 'dark',
notifications: true
}
}
}
Use cases:
- User profiles and preferences
- Todo lists and task management
- Shopping cart data
- Form drafts
- Application settings
2. Computed Values (computed)
Derived data that automatically updates when dependencies change. Great for calculations, formatted strings, and aggregated data.
{
id: 'displayName',
type: 'computed',
compute: (data) => {
const profile = data.userProfile
return `${profile?.name || 'Guest'} (${profile?.email || 'N/A'})`
},
dependencies: ['userProfile']
}
Use cases:
- Formatted display text
- Calculated totals and statistics
- Filtered/sorted lists
- Conditional values
- Data transformations
3. Static Data (static)
Constant values that don't change during the session. Useful for configuration and reference data.
{
id: 'appConfig',
type: 'static',
defaultValue: {
apiUrl: 'https://api.example.com',
version: '1.0.0',
features: ['chat', 'notifications']
}
}
Use cases:
- API endpoints and configuration
- Feature flags
- Reference data (countries, categories)
- Constants
- Initial form values
Binding Properties
Once you have data sources, bind them to component properties:
{
id: 'welcome-heading',
type: 'Heading',
bindings: {
children: {
source: 'displayName'
}
}
}
Path-based Bindings
Access nested properties using dot notation:
{
id: 'email-input',
type: 'Input',
bindings: {
value: {
source: 'userProfile',
path: 'email'
}
}
}
Transform Functions
Apply transformations to bound values:
{
id: 'price-display',
type: 'Text',
bindings: {
children: {
source: 'price',
transform: (value) => `$${(value / 100).toFixed(2)}`
}
}
}
Dependency Tracking
Computed sources automatically re-calculate when their dependencies change:
// Stats computed source depends on todos
{
id: 'stats',
type: 'computed',
compute: (data) => ({
total: data.todos?.length || 0,
completed: data.todos?.filter(t => t.completed).length || 0,
remaining: data.todos?.filter(t => !t.completed).length || 0
}),
dependencies: ['todos']
}
// When todos updates, stats automatically updates too
Best Practices
1. Use KV for Persistence
If data needs to survive page refreshes, use a KV source:
✅ { id: 'cart', type: 'kv', key: 'shopping-cart', defaultValue: [] }
❌ { id: 'cart', type: 'static', defaultValue: [] } // Will reset on refresh
2. Keep Computed Functions Pure
Computed functions should be deterministic and not have side effects:
✅ compute: (data) => data.items.filter(i => i.active)
❌ compute: (data) => {
toast.info('Computing...') // Side effect!
return data.items.filter(i => i.active)
}
3. Declare All Dependencies
Always list dependencies for computed sources:
✅ dependencies: ['todos', 'filter']
❌ dependencies: [] // Missing dependencies!
4. Use Meaningful IDs
Choose descriptive IDs that clearly indicate the data's purpose:
✅ id: 'userProfile'
✅ id: 'todoStats'
❌ id: 'data1'
❌ id: 'temp'
5. Structure Data Logically
Organize related data in nested objects:
✅ {
id: 'settings',
type: 'kv',
defaultValue: {
theme: 'dark',
notifications: true,
language: 'en'
}
}
❌ Multiple separate sources for related data
Complete Example
Here's a full example with multiple data sources and bindings:
{
dataSources: [
// KV storage for tasks
{
id: 'tasks',
type: 'kv',
key: 'user-tasks',
defaultValue: []
},
// Static filter options
{
id: 'filterOptions',
type: 'static',
defaultValue: ['all', 'active', 'completed']
},
// Current filter selection
{
id: 'currentFilter',
type: 'kv',
key: 'task-filter',
defaultValue: 'all'
},
// Computed filtered tasks
{
id: 'filteredTasks',
type: 'computed',
compute: (data) => {
const filter = data.currentFilter
const tasks = data.tasks || []
if (filter === 'all') return tasks
if (filter === 'active') return tasks.filter(t => !t.completed)
if (filter === 'completed') return tasks.filter(t => t.completed)
return tasks
},
dependencies: ['tasks', 'currentFilter']
},
// Computed statistics
{
id: 'taskStats',
type: 'computed',
compute: (data) => ({
total: data.tasks?.length || 0,
active: data.tasks?.filter(t => !t.completed).length || 0,
completed: data.tasks?.filter(t => t.completed).length || 0
}),
dependencies: ['tasks']
}
],
components: [
// Display total count
{
id: 'total-badge',
type: 'Badge',
bindings: {
children: {
source: 'taskStats',
path: 'total'
}
}
},
// List filtered tasks
{
id: 'task-list',
type: 'List',
bindings: {
items: {
source: 'filteredTasks'
}
}
}
]
}
UI Components
Data Source Manager
The DataSourceManager component provides a visual interface for creating and managing data sources:
- Create KV, computed, and static sources
- Edit source configuration
- View dependency relationships
- Delete sources (with safety checks)
Binding Editor
The BindingEditor component allows you to bind component properties to data sources:
- Select properties to bind
- Choose data sources
- Specify nested paths
- Preview bindings
Component Binding Dialog
Open a dialog to edit all bindings for a specific component with live preview.
Hooks
useDataSources
The core hook that manages all data sources:
import { useDataSources } from '@/hooks/data/use-data-sources'
const { data, updateData, updatePath, loading } = useDataSources(dataSources)
// Access data
const userProfile = data.userProfile
// Update entire source
updateData('userProfile', newProfile)
// Update nested property
updatePath('userProfile', 'email', 'newemail@example.com')
useDataSourceManager
Hook for managing the data source configuration:
import { useDataSourceManager } from '@/hooks/data/use-data-source-manager'
const {
dataSources,
addDataSource,
updateDataSource,
deleteDataSource,
getDataSource,
getDependents
} = useDataSourceManager(initialSources)
Tips & Tricks
Avoiding Circular Dependencies
Never create circular dependencies between computed sources:
❌ Bad:
{
id: 'a',
type: 'computed',
compute: (data) => data.b + 1,
dependencies: ['b']
},
{
id: 'b',
type: 'computed',
compute: (data) => data.a + 1,
dependencies: ['a']
}
Optimizing Computed Sources
Keep compute functions fast and efficient:
✅ Fast:
compute: (data) => data.items.length
❌ Slow:
compute: (data) => {
let result = 0
for (let i = 0; i < 1000000; i++) {
result += Math.random()
}
return result
}
Testing Data Sources
Test your data sources independently:
const source = {
id: 'stats',
type: 'computed',
compute: (data) => ({ total: data.items.length }),
dependencies: ['items']
}
const testData = { items: [1, 2, 3] }
const result = source.compute(testData)
// result: { total: 3 }