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

# Live Queries / Watch Queries

> Subscribe to live queries that re-run automatically when local SQLite data changes.

Watch queries, also known as live queries, are essential for building reactive apps where the UI automatically updates when the underlying data changes. PowerSync's watch functionality allows you to listen for SQL query result changes and receive updates whenever the dependent tables are modified.

# Overview

PowerSync provides multiple approaches to watching queries, each designed for different use cases and performance requirements:

1. Basic Watch Queries - These queries work across all SDKs, providing real-time updates when dependent tables change.
2. Incremental Watch Queries - Only emit updates when data actually changes, preventing unnecessary re-renders. Available in JavaScript SDKs only.
3. Differential Watch Queries - Provide detailed information about what specifically changed between result sets. Available in JavaScript SDKs only.

Choose the approach that best fits your platform and performance needs.

# Basic Watch Queries

PowerSync supports the following basic watch queries based on your platform. These APIs return query results whenever the underlying tables change and are available across all SDKs.

<Tabs>
  <Tab title="JavaScript">
    <CodeGroup>
      ```javascript AsyncIterator theme={null}
      // The original watch method using the AsyncIterator pattern. This is the
      // foundational watch API that works across all JavaScript environments.
      async function* pendingLists(): AsyncIterable<string[]> {
        for await (const result of db.watch(
          `SELECT * FROM lists WHERE state = ?`,
          ['pending']
        )) {
          yield result.rows?._array ?? [];
        }
      }
      ```

      ```javascript Callback theme={null}
      // Callback-based watch method that doesn't require AsyncIterator polyfills.
      // Use this approach when you need smoother React Native compatibility or
      // prefer synchronous method signatures.
      const pendingLists = (onResult: (lists: any[]) => void): void => {
        db.watch(
          'SELECT * FROM lists WHERE state = ?',
          ['pending'],
          {
            onResult: (result: any) => {
              onResult(result.rows?._array ?? []);
            }
          }
        );
      }
      ```

      ```javascript React Hook theme={null}
      // React hook that combines watch functionality with built-in loading,
      // fetching, and error states. Use this when you need convenient state
      // management without React Suspense.
      const {
        data: pendingLists,
        isLoading,
        isFetching,
        error
      } = useQuery('SELECT * FROM lists WHERE state = ?', ['pending']);
      ```

      ```javascript React Suspense theme={null}
      // React Suspense-based hook that automatically handles loading and error
      // states through Suspense boundaries. Use this when you want to leverage
      // React's concurrent features and avoid manual state handling.
      const { data: pendingLists } = useSuspenseQuery('SELECT * FROM lists WHERE state = ?', ['pending']);
      ```

      ```javascript Vue Hook theme={null}
      // Vue composition API hook with built-in loading, fetching, and error
      // states. Use this for reactive watch queries in Vue applications.
      const {
        data: pendingLists,
        isLoading,
        isFetching,
        error
      } = useQuery('SELECT * FROM lists WHERE state = ?', ['pending']);
      ```
    </CodeGroup>

    <Warning>
      The `db.watch()` AsyncIterator and Callback methods above are only maintained for backwards compatibility. Use the improved `db.query().watch()` API instead (see [Incremental Watch Queries](#incremental-watch-queries) below).
    </Warning>
  </Tab>

  <Tab title="Dart/Flutter">
    Use this method to watch for changes to the dependent tables of any SQL query:

    ```dart theme={null}
    StreamBuilder(
      stream: db.watch('SELECT * FROM lists WHERE state = ?', ['pending']),
      builder: (context, snapshot) {
        if (snapshot.hasData) {
          // TODO: implement your own UI here based on the result set
          return ...;
        } else {
          return const Center(child: CircularProgressIndicator());
        }
      },
    )
    ```
  </Tab>

  <Tab title="Kotlin">
    Use this method to watch for changes to the dependent tables of any SQL query:

    ```kotlin theme={null}
    fun watchPendingLists(): Flow<List<ListItem>> =
        db.watch(
            "SELECT * FROM lists WHERE state = ?",
            listOf("pending"),
        ) { cursor ->
            ListItem(
                id = cursor.getString("id"),
                name = cursor.getString("name"),
            )
        }
    ```
  </Tab>

  <Tab title="Swift">
    Use this method to watch for changes to the dependent tables of any SQL query:

    ```swift theme={null}
    func watchPendingLists() throws -> AsyncThrowingStream<[ListContent], Error> {
        try db.watch(
            sql: "SELECT * FROM lists WHERE state = ?",
            parameters: ["pending"],
        ) { cursor in
            try ListContent(
                id: cursor.getString(name: "id"),
                name: cursor.getString(name: "name"),
            )
        }
    }
    ```
  </Tab>

  <Tab title=".NET">
    Use this method to watch for changes to the dependent tables of any SQL query:

    ```csharp theme={null}
    // Define a result type with properties matching the schema columns (some columns omitted here for brevity)
    // public class ListResult { public string id; public string name; public string owner_id; ... }

    // Optional cancellation token to stop watching
    var cts = new CancellationTokenSource();

    // Register listener synchronously on the calling thread...
    var listener = db.Watch<ListResult>(
        "SELECT * FROM lists WHERE owner_id = ?",
        [ownerId],
        new SQLWatchOptions { Signal = cts.Token }
    );

    // ...then listen to changes on another thread (or await foreach directly if already in an async context)
    _ = Task.Run(async () =>
    {
        await foreach (var results in listener)
        {
            Console.WriteLine("Lists: ");
            foreach (var result in results)
            {
                Console.WriteLine($"{result.id}: {result.name}");
            }
        }
    }, cts.Token);

    // To stop watching, cancel the token: cts.Cancel();
    ```
  </Tab>

  <Tab title="Rust">
    Use this method to watch for changes to the dependent tables of any SQL query:

    ```Rust theme={null}
    async fn watch_pending_lists(db: &PowerSyncDatabase) -> Result<(), PowerSyncError> {
        let stream = db.watch_statement(
            "SELECT * FROM lists WHERE state = ?".to_string(),
            params!["pending"],
            |stmt, params| {
                let mut rows = stmt.query(params)?;
                let mut mapped = vec![];

                while let Some(row) = rows.next()? {
                    mapped.push(() /* TODO: Read row into list struct */)
                }

                Ok(mapped)
            },
        );
        let mut stream = pin!(stream);

        // Note: The stream is never-ending, so you probably want to call this in an independent async
        // task.
        while let Some(event) = stream.try_next().await? {
            // Update UI to display rows
        }
        Ok(())
    }
    ```
  </Tab>
</Tabs>

# Incremental Watch Queries

Basic watch queries can cause performance issues in UI frameworks like React because they return new data on every dependent table change, even when the actual data in the query hasn't changed. This can lead to excessive re-renders as components receive updates unnecessarily.

Incremental watch queries solve this by comparing result sets using configurable comparators and only emitting updates when the comparison detects actual data changes. These queries still query the SQLite database under the hood on each dependent table change, but compare the result sets and only yield results if a change has been made.

<Note>
  **JavaScript Only**: Incremental and differential watch queries are currently only available in the JavaScript SDKs starting from:

  * Web v1.25.0
  * React Native v1.23.1
  * Node.js v0.8.1
</Note>

Basic Syntax:

```javascript theme={null}
db.query({ sql: 'SELECT * FROM lists WHERE state = ?', parameters: ['pending'] }).watch();
```

## WatchedQuery

`WatchedQuery` class that comes with a better API in that it includes loading, fetching and error states, supports multiple listeners, automatic cleanup on PowerSync close, and the new `updateSettings()` API for dynamic parameter changes. This is the preferred approach for JavaScript SDKs:

<CodeGroup>
  ```javascript WatchedQuery (Default) theme={null}
  // Create an instance of a WatchedQuery
  const pendingLists = db
    .query({
      sql: 'SELECT * FROM lists WHERE state = ?',
      parameters: ['pending']
    })
    .watch();

  // The registerListener method can be used multiple times to listen for updates
  const dispose = pendingLists.registerListener({
    onData: (data) => {
      // This callback will be called whenever the data changes
      console.log('Data updated:', data);
    },
    onStateChange: (state) => {
      // This callback will be called whenever the state changes
      // The state contains metadata about the query, such as isFetching, isLoading, etc.
      console.log('State changed:', state.error, state.isFetching, state.isLoading, state.data);
    },
    onError: (error) => {
      // This callback will be called if the query fails
      console.error('Query error:', error);
    }
  });
  ```

  ```javascript WatchedQuery (With Comparator) theme={null}
  // WatchedQuery with configurable comparator that compares result sets before
  // emitting to listeners, preventing unnecessary listener invocations when
  // data hasn't changed. Use this when you want shared query instances plus
  // result set comparison for incremental updates.
  const pendingLists = db
    .query({
      sql: 'SELECT * FROM lists WHERE state = ?',
      parameters: ['pending']
    })
    .watch({
      comparator: {
        checkEquality: (current, previous) => {
          return JSON.stringify(current) === JSON.stringify(previous);
        }
      }
    });

  // Register listeners as before...
  ```
</CodeGroup>

## React Hooks

React hooks that preserve object references for unchanged items and use row-level comparators to minimize re-renders:

<CodeGroup>
  ```javascript useQuery theme={null}
  // Use this when you want built-in state management plus
  // incremental updates for React components.
  const {
    data: pendingLists,
    isLoading,
    isFetching,
    error
  } = useQuery('SELECT * FROM lists WHERE state = ?', ['pending'], {
    rowComparator: {
      keyBy: (item) => item.id,
      compareBy: (item) => JSON.stringify(item)
    }
  });
  ```

  ```javascript useSuspenseQuery theme={null}
  // Use this when you want concurrent React features, automatic
  // state handling, and memoization-friendly object stability.
  const { data: lists } = useSuspenseQuery('SELECT * FROM lists WHERE state = ?', ['pending'], {
    rowComparator: {
      keyBy: (item) => item.id,
      compareBy: (item) => JSON.stringify(item)
    }
  });
  ```

  ```jsx React.memo (Row-Level) theme={null}
  const TodoListsWidget = () => {
    const { data: lists } = useQuery('[SQL]', [...parameters], { rowComparator: DEFAULT_ROW_COMPARATOR });

    return (
      <Box>
        {
          // The individual row widgets will only re-render if the corresponding row has changed
          lists.map((listRecord) => (
            <TodoWidget key={listRecord.id} record={listRecord} />
          ))
        }
      </Box>
    );
  };

  const TodoWidget = React.memo(({ record }) => {
    return <Box>{record.name}</Box>;
  });
  ```
</CodeGroup>

Providing a `rowComparator` to the React hooks ensures that components only re-render when the query result actually changes. When combined with React memoization (e.g., `React.memo`) on row components that receive query row objects as props, this approach prevents unnecessary updates at the individual row component level, resulting in more efficient UI rendering.

## Existing Watch APIs (with Comparator)

The existing `AsyncIterator` and Callback `db.watch()` APIs also support incremental updates via a `comparator` option. Use these if you want to maintain the familiar patterns from the basic watch query API:

<CodeGroup>
  ```javascript AsyncIterator theme={null}
  async function* pendingLists(): AsyncIterable<string[]> {
    for await (const result of db.watch('SELECT * FROM lists WHERE state = ?', ['pending'], {
      comparator: {
        checkEquality: (current, previous) => JSON.stringify(current) === JSON.stringify(previous)
      }
    })) {
      yield result.rows?._array ?? [];
    }
  }
  ```

  ```javascript Callback theme={null}
  const pendingLists = (onResult: (lists: any[]) => void): void => {
    db.watch(
      'SELECT * FROM lists WHERE state = ?',
      ['pending'],
      {
        onResult: (result: any) => {
          onResult(result.rows?._array ?? []);
        }
      },
      {
        comparator: {
          checkEquality: (current, previous) => {
            // This comparator will only report updates if the data changes.
            return JSON.stringify(current) === JSON.stringify(previous);
          }
        }
      }
    );
  };
  ```
</CodeGroup>

# Differential Watch Queries

Differential watch queries go a step further than incremental watched queries by computing and reporting diffs between result sets (added/removed/updated items) while preserving object references for unchanged items. This enables more precise UI updates.

<Note>
  **JavaScript Only**: Incremental and differential watch queries are currently only available in the JavaScript SDKs starting from:

  * Web v1.25.0
  * React Native v1.23.1
  * Node.js v0.8.1
</Note>

<Tip>
  For large result sets where re-running and comparing full query results becomes expensive, consider using trigger-based table diffs. See [High Performance Diffs](/client-sdks/high-performance-diffs).
</Tip>

Basic syntax:

```javascript theme={null}
db.query({ sql: 'SELECT * FROM lists WHERE state = ?', parameters: ['pending'] }).differentialWatch();
```

Use differential watch when you need to know exactly which items were added, removed, or updated rather than re-processing entire result sets:

```javascript theme={null}
// Create an instance of a WatchedQuery
const pendingLists = db
  .query({
    sql: 'SELECT * FROM lists WHERE state = ?',
    parameters: ['pending']
  })
  .differentialWatch();

// The registerListener method can be used multiple times to listen for updates
const dispose = pendingLists.registerListener({
  onData: (data) => {
    // This callback will be called whenever the data changes
    console.log('Data updated:', data);
  },
  onStateChange: (state) => {
    // This callback will be called whenever the state changes
    // The state contains metadata about the query, such as isFetching, isLoading, etc.
    console.log('State changed:', state.error, state.isFetching, state.isLoading, state.data);
  },
  onError: (error) => {
    // This callback will be called if the query fails
    console.error('Query error:', error);
  },
  onDiff: (diff) => {
    // This callback will be called whenever the data changes.
    console.log('Data updated:', diff.added, diff.updated);
  }
});
```

By default, the `differentialWatch()` method uses a `DEFAULT_ROW_COMPARATOR`. This comparator identifies (keys) each row by its `id` column if present, or otherwise by the JSON string of the entire row. For row comparison, it uses the JSON string representation of the full row. This approach is generally safe and effective for most queries.

For some queries, performance could be improved by supplying a custom `rowComparator`. Such as comparing by a `hash` column generated or stored in SQLite. These hashes currently require manual implementation.

```javascript theme={null}
const pendingLists = db
  .query({
    sql: 'SELECT * FROM lists WHERE state = ?',
    parameters: ['pending']
  })
  .differentialWatch({
    rowComparator: {
      keyBy: (item) => item.id,
      compareBy: (item) => item._hash
    }
  });
```

<Tip>
  The [Yjs Document Collaboration Demo
  app](https://github.com/powersync-ja/powersync-js/tree/main/demos/yjs-react-supabase-text-collab) showcases the use of
  differential watch queries. New document updates are passed to Yjs for consolidation as they are synced. See the
  implementation
  [here](https://github.com/powersync-ja/powersync-js/blob/main/demos/yjs-react-supabase-text-collab/src/library/powersync/PowerSyncYjsProvider.ts)
  for more details.
</Tip>

# The `WatchedQuery` Class

Both incremental and differential queries use the new `WatchedQuery` class. This class, along with a new `query` method allows building instances of `WatchedQuery`s via the `watch` and `differentialWatch` methods:

```javascript theme={null}
const watchedQuery = db.query({ sql: 'SELECT * FROM lists', parameters: [] }).watch();
```

This class provides advanced features:

* Automatically reprocesses itself if the PowerSync schema has been updated with `updateSchema`.
* Automatically closes itself when the PowerSync client has been closed.
* Allows for the query parameters to be updated after instantiation.
* Allows shared listening to state changes.
* New `updateSettings` API for dynamic parameter updates (see below).

## Query Sharing

`WatchedQuery` instances can be shared across components:

```javascript theme={null}
// Create a shared query instance
const sharedListsQuery = db.query({ sql: 'SELECT * FROM lists WHERE state = ?', parameters: ['pending'] }).watch();

// Multiple components can listen to the same query
const dispose1 = sharedListsQuery.registerListener({
  onData: (data) => updatePendingListsDisplay(data)
});

const dispose2 = sharedListsQuery.registerListener({
  onData: (data) => updatePendingListsCount(data.length)
});
```

## Dynamic Parameter Updates

Update query parameters to affect all listeners of the query:

```javascript theme={null}
// Updates to query parameters can be performed in a single place, affecting all listeners
sharedListsQuery.updateSettings({
  query: new GetAllQuery({ sql: 'SELECT * FROM lists WHERE state = ?', parameters: ['canceled'] })
});
```

## React Hook for External WatchedQuery Instances

When you need to share query instances across components or manage their lifecycle independently from component mounting, use the `useWatchedQuerySubscription` hook. This is ideal for global state management, query caching, or when multiple components need to listen to the same data:

```javascript theme={null}
// Managing the WatchedQuery externally can extend its lifecycle and allow in-memory caching between components.
const pendingLists = db
  .query({
    sql: 'SELECT * FROM lists WHERE state = ?',
    parameters: ['pending']
  })
  .watch();

// In the component
export const MyComponent = () => {
  // In React one could import the `pendingLists` query or create a context provider for various queries
  const { data } = useWatchedQuerySubscription(pendingLists);

  return (
    <div>
      {data.map((item) => (
        <div key={item.id}>{item.name}</div>
      ))}
    </div>
  );
};
```
