Live Queries

    Description — Couchbase Lite JavaScript — Reactive Live Queries
    Related Content — SQL++ for Mobile | Query Resultsets | Indexing

    Overview

    Live queries provide real-time updates when the results of a query change. This makes them perfect for building reactive user interfaces that automatically update when data changes.

    When you create a live query, Couchbase Lite monitors the database and notifies you whenever documents that match the query are added, modified, or deleted.

    Creating Live Queries

    Create a live query by adding a change listener to your query:

    Example 1. Basic live query
        interface TaskResult {
            title: string;
        }
    
        const query = database.createQuery(`
        SELECT title FROM tasks
        WHERE completed = false
        ORDER BY createdAt DESC
    `);
    
        // Add change listener - receives array of results
        const token = query.addChangeListener<TaskResult>((results) => {
            console.log(`Query results updated: ${results.length} tasks`);
    
            // Update UI with new results
            results.forEach(row => {
                const task = row as TaskResult;
                console.log(`Task: ${task.title}`);
            });
    
            // Update your application UI
            updateTaskList(results);
        });
    
        // The listener is now active and will be called whenever results change

    Removing Listeners

    Always remove listeners when they’re no longer needed:

    Example 2. Remove listener
    const query = database.createQuery('SELECT * FROM tasks WHERE completed = false');
    const token = query.addChangeListener(_ => {
        console.log('Results updated');
    });
    
    // Later, remove the listener
    token.remove();
    console.log('Listener removed');

    Multiple Listeners

    You can add multiple listeners to the same query:

    Example 3. Multiple listeners
    const query = database.createQuery('SELECT * FROM tasks WHERE priority >= 3');
    
    // Listener 1: Update UI
    const uiToken = query.addChangeListener(results => {
        updateTaskList(results);
    });
    
    // Listener 2: Log changes
    const logToken = query.addChangeListener(results => {
        console.log(`High priority tasks: ${results.length}`);
    });
    
    // Listener 3: Send analytics
    const analyticsToken = query.addChangeListener(results => {
        trackMetric('high_priority_tasks', results.length);
    });
    
    // Each listener receives independent notifications
    // Remove listeners individually when done
    uiToken.remove();
    logToken.remove();
    analyticsToken.remove();

    Each listener receives independent notifications.

    TypeScript Support

    Type your live query results for type safety:

    Example 4. Typed live queries
        interface TaskResult {
            title: string;
            completed: boolean;
            priority: number;
            createdAt: string;
        }
    
        const query = database.createQuery(`
        SELECT title, completed, priority, createdAt
        FROM tasks
        WHERE completed = false
    `);
    
        // Add typed listener
        const token = query.addChangeListener<TaskResult>((results) => {
            results.forEach(t => {
                const task = t as TaskResult;
                const title: string = task.title;  // Type-safe
                const priority: number = task.priority;
    
                console.log(`${title} - Priority: ${priority}`);
            });
        });

    Error Handling

    Handle errors in live query listeners:

    Example 5. Error handling
    const query = database.createQuery('SELECT * FROM tasks');
    
    query.addChangeListener(results => {
        try {
        // Process results
            updateUI(results);
        } catch (error) {
            console.error('Error updating UI:', error);
        // Handle error without crashing the listener
        }
    });
    
    // Wrap listener registration in try-catch for setup errors
    try {
        query.addChangeListener(results => {
            processResults(results);
        });
    } catch (error) {
        console.error('Failed to create live query listener:', error);
    }

    Live Query Lifecycle

    Example 6. Lifecycle stages
    class TaskManager {
        private query: Query | null = null;
        private database: Database | null = null;
        private listenerToken: ListenerToken | null = null;
    
        async start() {
            this.database = await Database.open({name: "mydb", version: 1, collections: { tasks: {} }});
    
            // Create query
            this.query = this.database.createQuery(`
            SELECT * FROM tasks
            WHERE assignedTo = $userId
            AND completed = false
        `);
    
            // Set parameters
            this.query.parameters = { userId: 'current-user' };
    
            // Start listening
            this.listenerToken = this.query.addChangeListener(results => {
                this.onResultsChanged(results);
            });
    
            console.log('Live query started');
        }
    
        onResultsChanged(results: QueryAliases[]) {
            console.log(`Tasks updated: ${results.length}`);
            this.updateUI(results);
        }
    
        updateParameters(userId: string) {
        // Changing parameters triggers listener with new results
            if (this.query) {
                this.query.parameters = { userId };
            }
        }
    
        stop() {
        // Clean up listener
            if (this.listenerToken) {
                this.listenerToken.remove();
                this.listenerToken = null;
                console.log('Live query stopped');
            }
        }
    
        private updateUI(_: QueryAliases[]) {
        // Update application UI
        }
    }
    
    // Usage
    const manager = new TaskManager();
    await manager.start();  // Start listening
    
    // Later: change parameters (triggers update)
    manager.updateParameters('different-user');
    
    // Clean up when component unmounts
    manager.stop();

    Limitations

    Database Changes Only:

    • Live queries only respond to local database changes

    • They don’t poll external data sources

    • Replication changes trigger live query updates

    Query Constraints:

    • All SQL++ query limitations apply (see query-n1ql-mobile.adoc#limitations)

    • Complex queries may have performance implications

    • Index availability affects live query performance

    Browser Specific:

    • Live queries depend on browser staying active

    • Background tabs may throttle updates

    • Consider using Service Workers for background updates

    How to . . .

    .

    Dive Deeper . . .

    Mobile Forum | Blog | Tutorials

    .