> ## Documentation Index
> Fetch the complete documentation index at: https://docs.multisynq.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Writing a Multisynq View

> Learn how to build responsive, interactive views that display model state and handle user input while maintaining perfect synchronization

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

<Warning>
  **THE MOST IMPORTANT RULE**: Views must **NEVER** write directly to the model. All model changes must go through events.
</Warning>

<Tabs>
  <Tab title="✅ Correct View Pattern">
    **Views read from model, publish events for changes**

    ```js theme={null}
    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");
    ```
  </Tab>

  <Tab title="❌ Incorrect View Pattern">
    **Never directly modify the model**

    ```js theme={null}
    class BadView extends Multisynq.View {
        init() {
            this.model = this.wellKnownModel("GameModel");
            this.setupBadHandlers();
        }
        
        setupBadHandlers() {
            const button = document.getElementById('addPlayer');
            
            button.addEventListener('click', () => {
                // ❌ NEVER write directly to model
                this.model.playerCount++;
                this.model.players.push({ name: "New Player" });
                
                // ❌ NEVER call model methods directly from view
                this.model.startGame();
                
                // ❌ NEVER modify model properties
                this.model.gameState = "playing";
            });
        }
        
        handlePlayerMove(x, y) {
            // ❌ NEVER update model directly
            this.model.currentPlayer.position.x = x;
            this.model.currentPlayer.position.y = y;
            
            // This breaks synchronization!
        }
    }
    ```

    <Warning>
      Direct model modification will cause synchronization errors and break the multi-user experience.
    </Warning>
  </Tab>
</Tabs>

## View Architecture Patterns

<CardGroup cols={2}>
  <Card title="🏗️ Modular Design" icon="building">
    **Create specialized sub-views**

    ```js theme={null}
    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
    }
    ```
  </Card>

  <Card title="📡 Event Communication" icon="satellite">
    **Proper event-driven architecture**

    ```js theme={null}
    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);
        }
    }
    ```
  </Card>
</CardGroup>

## View Lifecycle and Updates

<Tabs>
  <Tab title="Efficient Rendering">
    **Optimize view updates for performance**

    ```js theme={null}
    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");
    ```
  </Tab>

  <Tab title="Real-time Updates">
    **Handle real-time model changes**

    ```js theme={null}
    class RealtimeView extends Multisynq.View {
        init() {
            this.model = this.wellKnownModel("GameModel");
            this.setupRealTimeUpdates();
            
            // Subscribe to all model changes
            this.subscribe("model", "*", this.onModelChange);
        }
        
        setupRealTimeUpdates() {
            // Different update frequencies for different elements
            this.startHighFrequencyUpdates(); // Player positions
            this.startMediumFrequencyUpdates(); // UI elements
            this.startLowFrequencyUpdates(); // Background elements
        }
        
        startHighFrequencyUpdates() {
            this.updatePlayerPositions();
            this.future(16).startHighFrequencyUpdates(); // 60fps
        }
        
        startMediumFrequencyUpdates() {
            this.updateUI();
            this.future(100).startMediumFrequencyUpdates(); // 10fps
        }
        
        startLowFrequencyUpdates() {
            this.updateBackground();
            this.future(1000).startLowFrequencyUpdates(); // 1fps
        }
        
        updatePlayerPositions() {
            // Read current positions from model
            for (const [playerId, player] of this.model.players) {
                this.interpolatePlayerMovement(playerId, player);
            }
        }
        
        interpolatePlayerMovement(playerId, player) {
            // Smooth movement between updates
            const element = this.getPlayerElement(playerId);
            if (element) {
                const currentPos = this.getElementPosition(element);
                const targetPos = player.position;
                
                // Smooth interpolation
                const smoothPos = {
                    x: currentPos.x + (targetPos.x - currentPos.x) * 0.1,
                    y: currentPos.y + (targetPos.y - currentPos.y) * 0.1
                };
                
                this.setElementPosition(element, smoothPos);
            }
        }
        
        onModelChange(event) {
            // React to specific model changes
            switch (event.type) {
                case "player-joined":
                    this.addPlayerToView(event.data);
                    break;
                case "player-left":
                    this.removePlayerFromView(event.data);
                    break;
                case "game-state-changed":
                    this.updateGameStateUI(event.data);
                    break;
            }
        }
    }

    RealtimeView.register("RealtimeView");
    ```
  </Tab>
</Tabs>

## Input Handling Best Practices

<AccordionGroup>
  <Accordion title="🎮 Game Input" icon="gamepad">
    **Handle game controls efficiently**

    ```js theme={null}
    class GameInputView extends Multisynq.View {
        init() {
            this.model = this.wellKnownModel("GameModel");
            this.inputState = {
                keys: new Set(),
                mouse: { x: 0, y: 0, buttons: new Set() }
            };
            
            this.setupInputHandlers();
            this.startInputProcessing();
        }
        
        setupInputHandlers() {
            // Track key states
            document.addEventListener('keydown', (e) => {
                if (!this.inputState.keys.has(e.key)) {
                    this.inputState.keys.add(e.key);
                    this.publish("input", "key-down", { key: e.key });
                }
            });
            
            document.addEventListener('keyup', (e) => {
                this.inputState.keys.delete(e.key);
                this.publish("input", "key-up", { key: e.key });
            });
            
            // Track mouse state
            this.canvas.addEventListener('mousemove', (e) => {
                const rect = this.canvas.getBoundingClientRect();
                this.inputState.mouse.x = e.clientX - rect.left;
                this.inputState.mouse.y = e.clientY - rect.top;
                
                // Only send if mouse moved significantly
                if (this.shouldSendMouseUpdate()) {
                    this.publish("input", "mouse-move", {
                        x: this.inputState.mouse.x,
                        y: this.inputState.mouse.y
                    });
                }
            });
            
            this.canvas.addEventListener('mousedown', (e) => {
                this.inputState.mouse.buttons.add(e.button);
                this.publish("input", "mouse-down", {
                    button: e.button,
                    x: this.inputState.mouse.x,
                    y: this.inputState.mouse.y
                });
            });
            
            this.canvas.addEventListener('mouseup', (e) => {
                this.inputState.mouse.buttons.delete(e.button);
                this.publish("input", "mouse-up", {
                    button: e.button,
                    x: this.inputState.mouse.x,
                    y: this.inputState.mouse.y
                });
            });
        }
        
        startInputProcessing() {
            this.processInputState();
        }
        
        processInputState() {
            // Send continuous input state for held keys
            if (this.inputState.keys.size > 0) {
                this.publish("input", "keys-held", {
                    keys: Array.from(this.inputState.keys)
                });
            }
            
            // Continue processing
            this.future(16).processInputState(); // 60fps
        }
        
        shouldSendMouseUpdate() {
            // Throttle mouse updates
            const now = Date.now();
            if (!this.lastMouseUpdate || now - this.lastMouseUpdate > 16) {
                this.lastMouseUpdate = now;
                return true;
            }
            return false;
        }
    }

    GameInputView.register("GameInputView");
    ```
  </Accordion>

  <Accordion title="📱 Touch Input" icon="mobile">
    **Handle mobile touch interactions**

    ```js theme={null}
    class TouchInputView extends Multisynq.View {
        init() {
            this.model = this.wellKnownModel("GameModel");
            this.touches = new Map();
            this.setupTouchHandlers();
        }
        
        setupTouchHandlers() {
            this.canvas.addEventListener('touchstart', (e) => {
                e.preventDefault();
                
                for (const touch of e.changedTouches) {
                    this.touches.set(touch.identifier, {
                        x: touch.clientX,
                        y: touch.clientY,
                        startTime: Date.now()
                    });
                    
                    this.publish("input", "touch-start", {
                        id: touch.identifier,
                        x: touch.clientX,
                        y: touch.clientY
                    });
                }
            });
            
            this.canvas.addEventListener('touchmove', (e) => {
                e.preventDefault();
                
                for (const touch of e.changedTouches) {
                    const touchData = this.touches.get(touch.identifier);
                    if (touchData) {
                        touchData.x = touch.clientX;
                        touchData.y = touch.clientY;
                        
                        this.publish("input", "touch-move", {
                            id: touch.identifier,
                            x: touch.clientX,
                            y: touch.clientY
                        });
                    }
                }
            });
            
            this.canvas.addEventListener('touchend', (e) => {
                e.preventDefault();
                
                for (const touch of e.changedTouches) {
                    const touchData = this.touches.get(touch.identifier);
                    if (touchData) {
                        const duration = Date.now() - touchData.startTime;
                        
                        this.publish("input", "touch-end", {
                            id: touch.identifier,
                            x: touch.clientX,
                            y: touch.clientY,
                            duration: duration
                        });
                        
                        // Check for tap vs swipe
                        if (duration < 200) {
                            this.publish("input", "tap", {
                                x: touch.clientX,
                                y: touch.clientY
                            });
                        }
                        
                        this.touches.delete(touch.identifier);
                    }
                }
            });
        }
    }

    TouchInputView.register("TouchInputView");
    ```
  </Accordion>

  <Accordion title="🎯 Anticipatory Input" icon="target">
    **Provide immediate feedback for responsive UI**

    ```js theme={null}
    class ResponsiveView extends Multisynq.View {
        init() {
            this.model = this.wellKnownModel("GameModel");
            this.predictedState = new Map(); // Local predictions
            this.setupAnticipatoryFeedback();
        }
        
        setupAnticipatoryFeedback() {
            this.canvas.addEventListener('click', (e) => {
                const rect = this.canvas.getBoundingClientRect();
                const x = e.clientX - rect.left;
                const y = e.clientY - rect.top;
                
                // 1. Immediate visual feedback
                this.showClickEffect(x, y);
                
                // 2. Predict what will happen
                this.predictPlayerMovement(x, y);
                
                // 3. Send event to model
                this.publish("input", "player-click", { x, y });
            });
            
            // Listen for model confirmation
            this.subscribe("player", "moved", this.onPlayerMoved);
        }
        
        showClickEffect(x, y) {
            // Immediate visual feedback
            const effect = document.createElement('div');
            effect.className = 'click-effect';
            effect.style.left = x + 'px';
            effect.style.top = y + 'px';
            document.body.appendChild(effect);
            
            // Remove after animation
            setTimeout(() => {
                effect.remove();
            }, 500);
        }
        
        predictPlayerMovement(targetX, targetY) {
            const myPlayerId = this.getMyPlayerId();
            const player = this.model.players.get(myPlayerId);
            
            if (player) {
                // Predict smooth movement
                this.predictedState.set(myPlayerId, {
                    startPos: { ...player.position },
                    targetPos: { x: targetX, y: targetY },
                    startTime: Date.now(),
                    duration: 1000 // Predicted animation time
                });
                
                this.animatePredictedMovement(myPlayerId);
            }
        }
        
        animatePredictedMovement(playerId) {
            const prediction = this.predictedState.get(playerId);
            if (!prediction) return;
            
            const elapsed = Date.now() - prediction.startTime;
            const progress = Math.min(elapsed / prediction.duration, 1);
            
            // Smooth interpolation
            const currentPos = {
                x: prediction.startPos.x + (prediction.targetPos.x - prediction.startPos.x) * progress,
                y: prediction.startPos.y + (prediction.targetPos.y - prediction.startPos.y) * progress
            };
            
            // Update visual position
            this.drawPlayerAt(playerId, currentPos);
            
            if (progress < 1) {
                requestAnimationFrame(() => this.animatePredictedMovement(playerId));
            } else {
                this.predictedState.delete(playerId);
            }
        }
        
        onPlayerMoved(data) {
            // Model has confirmed movement - stop prediction and use actual position
            if (this.predictedState.has(data.playerId)) {
                this.predictedState.delete(data.playerId);
            }
            
            // Update to actual position
            this.drawPlayerAt(data.playerId, data.position);
        }
    }

    ResponsiveView.register("ResponsiveView");
    ```
  </Accordion>
</AccordionGroup>

## Best Practices Summary

<CardGroup cols={2}>
  <Card title="🔒 Synchronization" icon="lock">
    **Maintain perfect sync**

    * Never write directly to model
    * Always use events for changes
    * Read model state for display
    * Handle predictions carefully

    ```js theme={null}
    // ✅ Always use events
    this.publish("input", "action", data);

    // ❌ Never modify directly
    this.model.property = value;
    ```
  </Card>

  <Card title="⚡ Performance" icon="bolt">
    **Optimize rendering**

    * Use efficient change detection
    * Implement partial updates
    * Throttle high-frequency events
    * Cache expensive calculations

    ```js theme={null}
    // ✅ Efficient updates
    if (this.hasChanged()) {
        this.updateDisplay();
    }
    ```
  </Card>

  <Card title="🎮 User Experience" icon="gamepad">
    **Responsive interaction**

    * Provide immediate feedback
    * Use predictive animations
    * Handle edge cases gracefully
    * Support multiple input methods

    ```js theme={null}
    // ✅ Immediate feedback
    this.showFeedback();
    this.publish("input", "action", data);
    ```
  </Card>

  <Card title="🏗️ Architecture" icon="building">
    **Clean organization**

    * Use modular sub-views
    * Separate concerns clearly
    * Handle lifecycle properly
    * Document event interfaces

    ```js theme={null}
    // ✅ Modular design
    this.gameView = new GameView();
    this.uiView = new UIView();
    ```
  </Card>
</CardGroup>

## View vs Model Responsibilities

<Tabs>
  <Tab title="✅ View Responsibilities">
    **What views should handle**

    ```js theme={null}
    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();
        }
    }
    ```
  </Tab>

  <Tab title="❌ Model Responsibilities">
    **What views should NOT handle**

    ```js theme={null}
    class ImproperView extends Multisynq.View {
        init() {
            this.model = this.wellKnownModel("GameModel");
        }
        
        handleClick() {
            // ❌ Don't do game logic in views
            const player = this.model.currentPlayer;
            player.health -= 10;
            
            if (player.health <= 0) {
                this.model.gameState = "game-over";
            }
            
            // ❌ Don't manage game state in views
            this.model.score += 100;
            this.model.level++;
            
            // ❌ Don't handle collision detection in views
            for (const other of this.model.enemies) {
                if (this.checkCollision(player, other)) {
                    other.destroy();
                }
            }
        }
        
        checkCollision(a, b) {
            // ❌ Game logic belongs in model
            return Math.abs(a.x - b.x) < 20 && Math.abs(a.y - b.y) < 20;
        }
    }
    ```

    <Warning>
      Game logic, state management, and business rules must stay in the model for proper synchronization.
    </Warning>
  </Tab>
</Tabs>

## Common Mistakes

<Warning>
  **Avoid these common view development errors:**
</Warning>

<AccordionGroup>
  <Accordion title="❌ Direct Model Mutation" icon="ban">
    ```js theme={null}
    // ❌ NEVER modify model directly
    view.addEventListener('click', () => {
        this.model.player.x = newX;           // Breaks sync
        this.model.gameState = "playing";     // Breaks sync
        this.model.players.push(newPlayer);   // Breaks sync
    });

    // ✅ Use events instead
    view.addEventListener('click', () => {
        this.publish("input", "move-player", { x: newX });
        this.publish("game", "start-game", {});
        this.publish("game", "add-player", { player: newPlayerData });
    });
    ```
  </Accordion>

  <Accordion title="❌ Model-View Ping-Pong" icon="ping-pong">
    ```js theme={null}
    // ❌ Don't create event cascades
    class BadView extends Multisynq.View {
        init() {
            this.subscribe("model", "needs-input", this.provideInput);
        }
        
        handleClick() {
            this.publish("model", "user-clicked", {});
        }
        
        provideInput() {
            // ❌ This creates cascading events
            this.publish("model", "input-response", { data: "..." });
        }
    }

    // ✅ Direct communication patterns
    class GoodView extends Multisynq.View {
        handleClick() {
            // ✅ Direct action, no ping-pong
            this.publish("input", "click", { x, y });
        }
    }
    ```
  </Accordion>

  <Accordion title="❌ Blocking Operations" icon="clock">
    ```js theme={null}
    // ❌ Don't block the UI thread
    class BlockingView extends Multisynq.View {
        processLargeDataset() {
            // ❌ This will freeze the UI
            for (let i = 0; i < 1000000; i++) {
                this.processItem(i);
            }
        }
    }

    // ✅ Use async processing
    class NonBlockingView extends Multisynq.View {
        async processLargeDataset() {
            // ✅ Process in chunks
            for (let i = 0; i < 1000000; i += 1000) {
                this.processChunk(i, i + 1000);
                await new Promise(resolve => setTimeout(resolve, 0));
            }
        }
    }
    ```
  </Accordion>
</AccordionGroup>

## Next Steps

<CardGroup cols={2}>
  <Card title="Writing a Multisynq Model" icon="database" href="/tutorials/writing-multisynq-model">
    Learn to build the synchronized models that power your views
  </Card>

  <Card title="Events & Pub-Sub" icon="satellite" href="/tutorials/events-pub-sub">
    Master communication between models and views
  </Card>

  <Card title="Simple Animation" icon="play" href="/tutorials/simple-animation">
    See view patterns in action with animated examples
  </Card>

  <Card title="Multi-user Chat" icon="chat" href="/tutorials/multiuser-chat">
    Build interactive multi-user interfaces
  </Card>
</CardGroup>

<Note>
  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.
</Note>
