Views are the interactive layer of Multisynq applications - they handle user input, display model state, and provide the user interface. Unlike models, views have full access to browser APIs and can use any JavaScript libraries. However, they must follow specific patterns to maintain synchronization.

Core Principle: Read-Only Model Access

THE MOST IMPORTANT RULE: Views must NEVER write directly to the model. All model changes must go through events.

Views read from model, publish events for changes

class GameView extends Multisynq.View {
    init() {
        this.model = this.wellKnownModel("GameModel");
        this.setupUI();
        this.startRenderLoop();
        
        // Subscribe to model events
        this.subscribe("game", "state-changed", this.updateDisplay);
        this.subscribe("game", "player-joined", this.addPlayerToUI);
        this.subscribe("game", "player-left", this.removePlayerFromUI);
    }
    
    setupUI() {
        this.canvas = document.getElementById('gameCanvas');
        this.ctx = this.canvas.getContext('2d');
        
        // ✅ Handle user input by publishing events
        this.canvas.addEventListener('click', (event) => {
            const rect = this.canvas.getBoundingClientRect();
            const x = event.clientX - rect.left;
            const y = event.clientY - rect.top;
            
            // ✅ Publish event for model to handle
            this.publish("input", "player-click", { x, y });
        });
        
        this.canvas.addEventListener('keydown', (event) => {
            // ✅ Send input events to model
            this.publish("input", "key-press", { 
                key: event.key,
                timestamp: Date.now() // View can use real time
            });
        });
    }
    
    startRenderLoop() {
        // ✅ Views can use setTimeout/setInterval
        this.renderFrame();
    }
    
    renderFrame() {
        // ✅ Read directly from model for efficiency
        this.drawBackground();
        this.drawPlayers();
        this.drawUI();
        
        // Continue render loop
        requestAnimationFrame(() => this.renderFrame());
    }
    
    drawPlayers() {
        // ✅ Read model state directly
        const players = this.model.players;
        
        this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
        
        for (const [playerId, player] of players) {
            this.ctx.fillStyle = player.color;
            this.ctx.fillRect(
                player.position.x - 10,
                player.position.y - 10,
                20, 20
            );
            
            // Draw player name
            this.ctx.fillStyle = 'white';
            this.ctx.fillText(
                player.name,
                player.position.x,
                player.position.y - 15
            );
        }
    }
    
    updateDisplay() {
        // ✅ React to model changes
        this.updateScore();
        this.updateGameState();
    }
    
    updateScore() {
        const scoreElement = document.getElementById('score');
        if (scoreElement) {
            // ✅ Read from model to update UI
            scoreElement.textContent = `Score: ${this.model.score}`;
        }
    }
}

GameView.register("GameView");

View Architecture Patterns

🏗️ Modular Design

Create specialized sub-views

class MainView extends Multisynq.View {
    init() {
        this.model = this.wellKnownModel("GameModel");
        
        // Create sub-views
        this.gameView = new GameCanvasView(this.model);
        this.uiView = new UIView(this.model);
        this.chatView = new ChatView(this.model);
    }
}

class GameCanvasView extends Multisynq.View {
    init(model) {
        this.model = model;
        this.setupCanvas();
        this.startRendering();
    }
    
    // Specialized game rendering
}

class UIView extends Multisynq.View {
    init(model) {
        this.model = model;
        this.setupUI();
        this.bindEvents();
    }
    
    // UI management
}

📡 Event Communication

Proper event-driven architecture

class PlayerView extends Multisynq.View {
    init() {
        this.model = this.wellKnownModel("GameModel");
        
        // Listen to model events
        this.subscribe("player", "moved", this.onPlayerMove);
        this.subscribe("player", "died", this.onPlayerDeath);
        this.subscribe("game", "ended", this.onGameEnd);
        
        this.setupInputHandlers();
    }
    
    setupInputHandlers() {
        // Convert user input to events
        document.addEventListener('keydown', (e) => {
            this.publish("input", "key-down", {
                key: e.key,
                playerId: this.getMyPlayerId()
            });
        });
    }
    
    onPlayerMove(data) {
        // React to model changes
        this.updatePlayerPosition(data.playerId, data.position);
    }
}

View Lifecycle and Updates

Optimize view updates for performance

class PerformantGameView extends Multisynq.View {
    init() {
        this.model = this.wellKnownModel("GameModel");
        this.canvas = document.getElementById('canvas');
        this.ctx = this.canvas.getContext('2d');
        
        // Track what needs updating
        this.dirtyRegions = new Set();
        this.lastModelState = null;
        
        this.startRenderLoop();
        
        // Listen for specific model changes
        this.subscribe("game", "player-moved", this.markPlayerDirty);
        this.subscribe("game", "object-changed", this.markObjectDirty);
    }
    
    startRenderLoop() {
        this.renderFrame();
    }
    
    renderFrame() {
        // Only update if model changed
        if (this.hasModelChanged()) {
            this.updateChangedElements();
            this.lastModelState = this.getModelSnapshot();
        }
        
        requestAnimationFrame(() => this.renderFrame());
    }
    
    hasModelChanged() {
        // Efficient change detection
        const currentState = this.getModelSnapshot();
        return JSON.stringify(currentState) !== JSON.stringify(this.lastModelState);
    }
    
    getModelSnapshot() {
        // Create lightweight state snapshot
        return {
            playerCount: this.model.players.size,
            gameState: this.model.gameState,
            lastUpdate: this.model.lastUpdateTime
        };
    }
    
    updateChangedElements() {
        // Only redraw changed regions
        for (const region of this.dirtyRegions) {
            this.redrawRegion(region);
        }
        this.dirtyRegions.clear();
    }
    
    markPlayerDirty(data) {
        this.dirtyRegions.add(`player-${data.playerId}`);
    }
    
    markObjectDirty(data) {
        this.dirtyRegions.add(`object-${data.objectId}`);
    }
    
    redrawRegion(region) {
        // Efficient partial redraws
        if (region.startsWith('player-')) {
            const playerId = region.replace('player-', '');
            this.drawPlayer(playerId);
        } else if (region.startsWith('object-')) {
            const objectId = region.replace('object-', '');
            this.drawObject(objectId);
        }
    }
}

PerformantGameView.register("PerformantGameView");

Input Handling Best Practices

Best Practices Summary

🔒 Synchronization

Maintain perfect sync

  • Never write directly to model
  • Always use events for changes
  • Read model state for display
  • Handle predictions carefully
// ✅ Always use events
this.publish("input", "action", data);

// ❌ Never modify directly
this.model.property = value;

⚡ Performance

Optimize rendering

  • Use efficient change detection
  • Implement partial updates
  • Throttle high-frequency events
  • Cache expensive calculations
// ✅ Efficient updates
if (this.hasChanged()) {
    this.updateDisplay();
}

🎮 User Experience

Responsive interaction

  • Provide immediate feedback
  • Use predictive animations
  • Handle edge cases gracefully
  • Support multiple input methods
// ✅ Immediate feedback
this.showFeedback();
this.publish("input", "action", data);

🏗️ Architecture

Clean organization

  • Use modular sub-views
  • Separate concerns clearly
  • Handle lifecycle properly
  • Document event interfaces
// ✅ Modular design
this.gameView = new GameView();
this.uiView = new UIView();

View vs Model Responsibilities

What views should handle

class ProperView extends Multisynq.View {
    init() {
        // ✅ UI management
        this.setupUserInterface();
        
        // ✅ Input handling
        this.setupInputHandlers();
        
        // ✅ Display updates
        this.startRenderLoop();
        
        // ✅ Animation and effects
        this.setupAnimations();
        
        // ✅ Browser API access
        this.setupAudio();
        this.setupLocalStorage();
        
        // ✅ External libraries
        this.setupThreeJS();
        this.setupChartJS();
    }
    
    handleUserInput() {
        // ✅ Convert input to events
        this.publish("input", "user-action", data);
    }
    
    updateDisplay() {
        // ✅ Read model state and update visuals
        const gameState = this.model.gameState;
        this.renderGameState(gameState);
    }
    
    playSound(soundName) {
        // ✅ Views can use browser APIs
        const audio = new Audio(`sounds/${soundName}.mp3`);
        audio.play();
    }
}

Common Mistakes

Avoid these common view development errors:

Next Steps

Views are where your application comes to life for users. By following the read-only model principle and using events for all changes, you can build responsive, interactive interfaces that work perfectly across all users while maintaining the deterministic behavior required for synchronization.