Files
spore-ui/docs/STATE_PRESERVATION.md
2025-08-28 10:21:14 +02:00

8.4 KiB

SPORE UI State Preservation System

Overview

The SPORE UI framework now includes an advanced state preservation system that prevents UI state loss during data refreshes. This system ensures that user interactions like expanded cards, active tabs, and other UI state are maintained when data is updated from the server.

Key Features

1. UI State Persistence

  • Expanded Cards: When cluster member cards are expanded, their state is preserved across data refreshes
  • Active Tabs: Active tab selections within node detail views are maintained
  • User Interactions: All user-initiated UI changes are stored and restored automatically

2. Smart Data Updates

  • Change Detection: The system detects when data has actually changed and only updates what's necessary
  • Partial Updates: Components can update specific data without re-rendering the entire UI
  • State Preservation: UI state is automatically preserved during all data operations

3. Efficient Rendering

  • No Full Re-renders: Components avoid unnecessary full re-renders when only data changes
  • Granular Updates: Only changed properties trigger UI updates
  • Performance Optimization: Reduced DOM manipulation and improved user experience

Architecture

Enhanced ViewModel Class

The base ViewModel class now includes:

class ViewModel {
    // UI State Management
    setUIState(key, value)           // Store UI state
    getUIState(key)                  // Retrieve UI state
    getAllUIState()                  // Get all stored UI state
    clearUIState(key)                // Clear specific or all UI state
    
    // Change Detection
    hasChanged(property)             // Check if property changed
    getPrevious(property)            // Get previous value
    
    // Batch Updates
    batchUpdate(updates, options)    // Update multiple properties with state preservation
}

Enhanced Component Class

The base Component class now includes:

class Component {
    // UI State Management
    setUIState(key, value)           // Store local UI state
    getUIState(key)                  // Get local or view model state
    getAllUIState()                  // Get merged state
    restoreUIState()                 // Restore state from view model
    
    // Partial Updates
    updatePartial(property, newValue, previousValue)  // Handle partial updates
}

Implementation Examples

1. Cluster Members Component

The ClusterMembersComponent demonstrates state preservation:

class ClusterMembersComponent extends Component {
    setupViewModelListeners() {
        // Listen with change detection
        this.subscribeToProperty('members', this.handleMembersUpdate.bind(this));
    }
    
    handleMembersUpdate(newMembers, previousMembers) {
        if (this.shouldPreserveState(newMembers, previousMembers)) {
            // Partial update preserves UI state
            this.updateMembersPartially(newMembers, previousMembers);
        } else {
            // Full re-render only when necessary
            this.render();
        }
    }
    
    shouldPreserveState(newMembers, previousMembers) {
        // Check if member structure allows state preservation
        if (newMembers.length !== previousMembers.length) return false;
        
        const newIps = new Set(newMembers.map(m => m.ip));
        const prevIps = new Set(previousMembers.map(m => m.ip));
        
        return newIps.size === prevIps.size && 
               [...newIps].every(ip => prevIps.has(ip));
    }
}

2. Node Details Component

The NodeDetailsComponent preserves active tab state:

class NodeDetailsComponent extends Component {
    setupViewModelListeners() {
        this.subscribeToProperty('activeTab', this.handleActiveTabUpdate.bind(this));
    }
    
    handleActiveTabUpdate(newTab, previousTab) {
        // Update tab UI without full re-render
        this.updateActiveTab(newTab, previousTab);
    }
    
    updateActiveTab(newTab) {
        // Update only the tab UI, preserving other state
        const tabButtons = this.findAllElements('.tab-button');
        const tabContents = this.findAllElements('.tab-content');
        
        tabButtons.forEach(btn => btn.classList.remove('active'));
        tabContents.forEach(content => content.classList.remove('active'));
        
        const activeButton = this.findElement(`[data-tab="${newTab}"]`);
        const activeContent = this.findElement(`#${newTab}-tab`);
        
        if (activeButton) activeButton.classList.add('active');
        if (activeContent) activeContent.classList.add('active');
    }
}

Usage Patterns

1. Storing UI State

// In a component
this.setUIState('expandedCard', memberIp);
this.setUIState('activeTab', 'firmware');

// In a view model
this.setUIState('userPreferences', { theme: 'dark', layout: 'compact' });

2. Retrieving UI State

// Get specific state
const expandedCard = this.getUIState('expandedCard');
const activeTab = this.getUIState('activeTab');

// Get all state
const allState = this.getAllUIState();

3. Batch Updates with State Preservation

// Update data while preserving UI state
this.viewModel.batchUpdate({
    members: newMembers,
    lastUpdateTime: new Date().toISOString()
}, { preserveUIState: true });

4. Smart Updates

// Use smart update to preserve state
await this.viewModel.smartUpdate();

Benefits

1. Improved User Experience

  • Users don't lose their place in the interface
  • Expanded cards remain expanded
  • Active tabs stay selected
  • No jarring UI resets

2. Better Performance

  • Reduced unnecessary DOM manipulation
  • Efficient partial updates
  • Optimized rendering cycles

3. Maintainable Code

  • Clear separation of concerns
  • Consistent state management patterns
  • Easy to extend and modify

Testing

Use the test-state-preservation.html file to test the state preservation system:

  1. Expand cluster member cards
  2. Change active tabs in node details
  3. Trigger data refresh
  4. Verify state is preserved

Migration Guide

From Old System

If you're upgrading from the old system:

  1. Update ViewModel Listeners: Change from this.render.bind(this) to specific update handlers
  2. Add State Management: Use setUIState() and getUIState() for UI state
  3. Implement Partial Updates: Override updatePartial() method for efficient updates
  4. Use Smart Updates: Replace direct data updates with smartUpdate() calls

Example Migration

Old Code:

this.subscribeToProperty('members', this.render.bind(this));

async handleRefresh() {
    await this.viewModel.updateClusterMembers();
}

New Code:

this.subscribeToProperty('members', this.handleMembersUpdate.bind(this));

async handleRefresh() {
    await this.viewModel.smartUpdate();
}

handleMembersUpdate(newMembers, previousMembers) {
    if (this.shouldPreserveState(newMembers, previousMembers)) {
        this.updateMembersPartially(newMembers, previousMembers);
    } else {
        this.render();
    }
}

Best Practices

  1. Always Store UI State: Use setUIState() for any user interaction
  2. Implement Partial Updates: Override updatePartial() for efficient updates
  3. Use Change Detection: Leverage hasChanged() to avoid unnecessary updates
  4. Batch Related Updates: Use batchUpdate() for multiple property changes
  5. Test State Preservation: Verify that UI state is maintained during data refreshes

Troubleshooting

Common Issues

  1. State Not Preserved: Ensure you're using setUIState() and getUIState()
  2. Full Re-renders: Check if shouldPreserveState() logic is correct
  3. Performance Issues: Verify you're using partial updates instead of full renders

Debug Tips

  1. Enable Console Logging: Check browser console for state preservation logs
  2. Use State Indicators: Monitor state changes in the test interface
  3. Verify Change Detection: Ensure hasChanged() is working correctly

Future Enhancements

  • State Synchronization: Real-time state sync across multiple browser tabs
  • Advanced Change Detection: Deep object comparison for complex data structures
  • State Persistence: Save UI state to localStorage for session persistence
  • State Rollback: Ability to revert to previous UI states