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

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 }